@looopy-ai/aws 1.0.1
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/LICENSE +9 -0
- package/README.md +156 -0
- package/dist/agentcore-runtime-server.d.ts +14 -0
- package/dist/agentcore-runtime-server.js +113 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/stores/agentcore-memory-message-store.d.ts +36 -0
- package/dist/stores/agentcore-memory-message-store.js +179 -0
- package/dist/stores/dynamodb-agent-store.d.ts +33 -0
- package/dist/stores/dynamodb-agent-store.js +88 -0
- package/dist/stores/index.d.ts +2 -0
- package/dist/stores/index.js +2 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Greg Bacchus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# @looopy-ai/aws
|
|
2
|
+
|
|
3
|
+
AWS helpers for running Looopy AgentCore inside AWS runtimes. This package currently ships a DynamoDB-backed `AgentStore` plus the `agentcore-runtime-server` used to service Bedrock AgentCore Runtime events.
|
|
4
|
+
|
|
5
|
+
## DynamoDB Agent Store
|
|
6
|
+
|
|
7
|
+
`DynamoDBAgentStore` persists each AgentCore context in DynamoDB. A single table can host many agents by prefixing the keys:
|
|
8
|
+
|
|
9
|
+
- Partition key (`pk` by default): `agent#{agentId}`
|
|
10
|
+
- Sort key (`sk` by default): `context#{contextId}`
|
|
11
|
+
- Attributes: `entityType`, serialized `state`, and an ISO `updatedAt`
|
|
12
|
+
|
|
13
|
+
You can override the key attribute names and prefixes in the constructor to match an existing schema.
|
|
14
|
+
|
|
15
|
+
### Usage
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { Agent } from '@looopy-ai/core';
|
|
19
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
20
|
+
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
21
|
+
import { DynamoDBAgentStore } from '@looopy-ai/aws/ts/stores';
|
|
22
|
+
|
|
23
|
+
const documentClient = DynamoDBDocumentClient.from(
|
|
24
|
+
new DynamoDBClient({ region: process.env.AWS_REGION }),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const agentStore = new DynamoDBAgentStore({
|
|
28
|
+
tableName: process.env.AGENT_STATE_TABLE!,
|
|
29
|
+
agentId: 'agentcore-runtime',
|
|
30
|
+
documentClient,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const agent = new Agent({
|
|
34
|
+
agentId: 'agentcore-runtime',
|
|
35
|
+
contextId: 'ctx-1234',
|
|
36
|
+
agentStore,
|
|
37
|
+
// supply llmProvider, toolProviders, and messageStore
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Creating the table with AWS CDK
|
|
42
|
+
|
|
43
|
+
The snippet below provisions a table that matches the defaults used by `DynamoDBAgentStore`.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { Stack, StackProps, RemovalPolicy, CfnOutput } from 'aws-cdk-lib';
|
|
47
|
+
import { Construct } from 'constructs';
|
|
48
|
+
import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb';
|
|
49
|
+
|
|
50
|
+
export class AgentStateStoreStack extends Stack {
|
|
51
|
+
public readonly table: Table;
|
|
52
|
+
|
|
53
|
+
constructor(scope: Construct, id: string, props?: StackProps) {
|
|
54
|
+
super(scope, id, props);
|
|
55
|
+
|
|
56
|
+
this.table = new Table(this, 'AgentStateTable', {
|
|
57
|
+
partitionKey: { name: 'pk', type: AttributeType.STRING },
|
|
58
|
+
sortKey: { name: 'sk', type: AttributeType.STRING },
|
|
59
|
+
billingMode: BillingMode.PAY_PER_REQUEST,
|
|
60
|
+
pointInTimeRecovery: true,
|
|
61
|
+
removalPolicy: RemovalPolicy.RETAIN,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
new CfnOutput(this, 'AgentStateTableName', { value: this.table.tableName });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
1. Run `cdk init app --language typescript` in a new directory.
|
|
70
|
+
2. Add the stack above to `lib/agent-state-store-stack.ts` and synthesize with `cdk synth`.
|
|
71
|
+
3. Deploy with `cdk deploy` and capture the `AgentStateTableName` output.
|
|
72
|
+
4. Pass the table name to your runtime as `AGENT_STATE_TABLE` and grant the runtime IAM role `dynamodb:GetItem`, `PutItem`, and `DeleteItem` actions for that table.
|
|
73
|
+
|
|
74
|
+
> Optional: add `timeToLiveAttribute: 'ttl'` to the table props and write a UNIX timestamp to that attribute from your runtime if you want DynamoDB TTL based cleanup.
|
|
75
|
+
|
|
76
|
+
### Wiring into AgentCore Runtime Server
|
|
77
|
+
|
|
78
|
+
When running the provided `agentcore-runtime-server`, create the store once and reuse it for each invocation:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { serve } from '@looopy-ai/aws';
|
|
82
|
+
import { Agent } from '@looopy-ai/core';
|
|
83
|
+
import { DynamoDBAgentStore } from '@looopy-ai/aws/ts/stores';
|
|
84
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
85
|
+
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
86
|
+
|
|
87
|
+
const documentClient = DynamoDBDocumentClient.from(new DynamoDBClient({ region: process.env.AWS_REGION }));
|
|
88
|
+
const agentStore = new DynamoDBAgentStore({
|
|
89
|
+
tableName: process.env.AGENT_STATE_TABLE!,
|
|
90
|
+
agentId: 'agentcore-runtime',
|
|
91
|
+
documentClient,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
serve({
|
|
95
|
+
agent: async (contextId) =>
|
|
96
|
+
new Agent({
|
|
97
|
+
agentId: 'agentcore-runtime',
|
|
98
|
+
contextId,
|
|
99
|
+
agentStore,
|
|
100
|
+
// other dependencies here
|
|
101
|
+
}),
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
This ensures each Bedrock AgentCore request can resume from the state stored in DynamoDB across separate Lambda or container invocations.
|
|
106
|
+
|
|
107
|
+
## AgentCore Memory Message Store
|
|
108
|
+
|
|
109
|
+
`AgentCoreMemoryMessageStore` streams conversation turns to the Bedrock AgentCore Memory APIs so short-term and long-term memories persist outside of your runtime container. The store wraps the runtime (`CreateEvent`, `ListEvents`, `DeleteEvent`) and memory retrieval APIs (`RetrieveMemoryRecords`) and implements the `MessageStore` contract used by `Agent`.
|
|
110
|
+
|
|
111
|
+
### Prerequisites
|
|
112
|
+
|
|
113
|
+
1. In the AWS Console open **Amazon Bedrock → Agentic Memory** (or use the `@aws-sdk/client-bedrock-agentcore` / AWS CLI equivalent) and create a Memory resource. Enable the strategies you want (summaries, user preferences, etc.).
|
|
114
|
+
2. Capture the `memoryId` that is returned.
|
|
115
|
+
3. Grant the runtime IAM role the following permissions scoped to that memory: `bedrock:CreateEvent`, `bedrock:ListEvents`, `bedrock:DeleteEvent`, and `bedrock:RetrieveMemoryRecords`.
|
|
116
|
+
4. Provide an `agentId` that serves as the actor identifier for all sessions. This typically represents the agent or assistant identity.
|
|
117
|
+
|
|
118
|
+
### Usage
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import { Agent } from '@looopy-ai/core';
|
|
122
|
+
import { AgentCoreMemoryMessageStore } from '@looopy-ai/aws/ts/stores';
|
|
123
|
+
|
|
124
|
+
const messageStore = new AgentCoreMemoryMessageStore({
|
|
125
|
+
memoryId: process.env.AGENT_MEMORY_ID!,
|
|
126
|
+
agentId: 'agentcore-runtime',
|
|
127
|
+
region: process.env.AWS_REGION,
|
|
128
|
+
// Optional: enable long-term memory retrieval
|
|
129
|
+
longTermMemoryNamespace: 'persistent-context',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const agent = new Agent({
|
|
133
|
+
agentId: 'agentcore-runtime',
|
|
134
|
+
contextId: 'ctx-1234',
|
|
135
|
+
messageStore,
|
|
136
|
+
// llmProvider, toolProviders, agentStore, etc.
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Features
|
|
141
|
+
|
|
142
|
+
- **Short-term memory:** Every call to `append` persists conversation turns via `CreateEventCommand`. Messages are stored per session (`contextId`).
|
|
143
|
+
- **Long-term memory (optional):** When `longTermMemoryNamespace` is configured, `getRecent` automatically retrieves and prepends relevant long-term memories to the conversation context.
|
|
144
|
+
- **Token budget support:** `getRecent` honors token limits using `trimToTokenBudget` to keep conversations within model constraints.
|
|
145
|
+
- **Memory search:** Use `searchMemories(query, options?)` to retrieve long-term memories semantically related to a query.
|
|
146
|
+
- **Session cleanup:** Call `clear(contextId)` to remove all short-term events for a specific session. Long-term memories persist per your memory resource configuration.
|
|
147
|
+
|
|
148
|
+
### Configuration Options
|
|
149
|
+
|
|
150
|
+
| Option | Required | Description |
|
|
151
|
+
|--------|----------|-------------|
|
|
152
|
+
| `memoryId` | Yes | Pre-provisioned AgentCore memory identifier |
|
|
153
|
+
| `agentId` | Yes | Static actor identifier used across all sessions |
|
|
154
|
+
| `region` | No | AWS region (defaults to `AWS_REGION` env var or `us-west-2`) |
|
|
155
|
+
| `client` | No | Custom `BedrockAgentCoreClient` instance (useful for testing) |
|
|
156
|
+
| `longTermMemoryNamespace` | No | Namespace for retrieving long-term memories. When set, enables automatic memory retrieval in `getRecent` |
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Agent, type AuthContext } from '@looopy-ai/core';
|
|
2
|
+
import type pino from 'pino';
|
|
3
|
+
type ServeConfig = {
|
|
4
|
+
agent: (contextId: string) => Promise<Agent>;
|
|
5
|
+
decodeAuthorization?: (authorization: string) => Promise<AuthContext | null>;
|
|
6
|
+
port?: number;
|
|
7
|
+
};
|
|
8
|
+
declare module 'hono' {
|
|
9
|
+
interface ContextVariableMap {
|
|
10
|
+
logger: pino.Logger;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export declare const serve: (config: ServeConfig) => void;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { serve as serveNodeJs } from '@hono/node-server';
|
|
2
|
+
import { getLogger } from '@looopy-ai/core';
|
|
3
|
+
import { SSEServer } from '@looopy-ai/core/ts';
|
|
4
|
+
import { Hono } from 'hono';
|
|
5
|
+
import { requestId } from 'hono/request-id';
|
|
6
|
+
import { pinoHttp } from 'pino-http';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
const promptValidator = z.object({
|
|
9
|
+
prompt: z.string().min(1),
|
|
10
|
+
});
|
|
11
|
+
export const serve = (config) => {
|
|
12
|
+
const app = new Hono();
|
|
13
|
+
app.use(requestId());
|
|
14
|
+
app.use(async (c, next) => {
|
|
15
|
+
c.env.incoming.id = c.var.requestId;
|
|
16
|
+
await new Promise((resolve) => pinoHttp({ logger: getLogger({}) })(c.env.incoming, c.env.outgoing, () => resolve(undefined)));
|
|
17
|
+
c.set('logger', c.env.incoming.log);
|
|
18
|
+
await next();
|
|
19
|
+
});
|
|
20
|
+
const state = { busy: false, agent: undefined };
|
|
21
|
+
app.get('/ping', async (c) => {
|
|
22
|
+
return c.text(JSON.stringify({
|
|
23
|
+
status: state.busy ? 'HealthyBusy' : 'Healthy',
|
|
24
|
+
time_of_last_update: Date.now(),
|
|
25
|
+
}));
|
|
26
|
+
});
|
|
27
|
+
app.post('/invocation', async (c) => {
|
|
28
|
+
const logger = c.var.logger;
|
|
29
|
+
if (state.busy) {
|
|
30
|
+
return c.json({ error: 'Agent is currently busy' }, 503);
|
|
31
|
+
}
|
|
32
|
+
state.busy = true;
|
|
33
|
+
const contextId = c.req.header('X-Amzn-Bedrock-AgentCore-Runtime-Session-Id') || undefined;
|
|
34
|
+
if (!contextId) {
|
|
35
|
+
state.busy = false;
|
|
36
|
+
return c.json({ error: 'Missing X-Amzn-Bedrock-AgentCore-Runtime-Session-Id header' }, 400);
|
|
37
|
+
}
|
|
38
|
+
const authorization = c.req.header('Authorization') || undefined;
|
|
39
|
+
if (!authorization && config.decodeAuthorization) {
|
|
40
|
+
state.busy = false;
|
|
41
|
+
return c.json({ error: 'Missing Authorization header' }, 401);
|
|
42
|
+
}
|
|
43
|
+
const authContext = await getAuthContext(authorization, config.decodeAuthorization);
|
|
44
|
+
if (config.decodeAuthorization && !authContext) {
|
|
45
|
+
state.busy = false;
|
|
46
|
+
return c.json({ error: 'Forbidden' }, 403);
|
|
47
|
+
}
|
|
48
|
+
if (!state.agent) {
|
|
49
|
+
state.agent = await config.agent(contextId);
|
|
50
|
+
logger.info({ contextId }, 'Created new agent instance');
|
|
51
|
+
}
|
|
52
|
+
const agent = state.agent;
|
|
53
|
+
if (agent.contextId !== contextId) {
|
|
54
|
+
state.busy = false;
|
|
55
|
+
return c.json({ error: 'Another session is active' }, 409);
|
|
56
|
+
}
|
|
57
|
+
const body = await c.req.json();
|
|
58
|
+
const promptValidation = promptValidator.safeParse(body);
|
|
59
|
+
if (!promptValidation.success) {
|
|
60
|
+
state.busy = false;
|
|
61
|
+
return c.json({ error: 'Invalid prompt', details: promptValidation.error.issues }, 400);
|
|
62
|
+
}
|
|
63
|
+
const { prompt } = promptValidation.data;
|
|
64
|
+
const sseServer = new SSEServer();
|
|
65
|
+
const turn = await agent.startTurn(prompt);
|
|
66
|
+
turn.subscribe({
|
|
67
|
+
next: (evt) => {
|
|
68
|
+
sseServer.emit(contextId, evt);
|
|
69
|
+
},
|
|
70
|
+
complete: async () => {
|
|
71
|
+
sseServer.shutdown();
|
|
72
|
+
state.busy = false;
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
const res = c.res;
|
|
76
|
+
logger.info({ contextId }, 'SSE connection established');
|
|
77
|
+
const stream = new ReadableStream({
|
|
78
|
+
start(controller) {
|
|
79
|
+
sseServer.subscribe({
|
|
80
|
+
setHeader: (name, value) => {
|
|
81
|
+
res.headers.set(name, value);
|
|
82
|
+
},
|
|
83
|
+
write: (chunk) => {
|
|
84
|
+
controller.enqueue(new TextEncoder().encode(chunk));
|
|
85
|
+
},
|
|
86
|
+
end: function () {
|
|
87
|
+
logger.info({ contextId }, 'SSE stream finished');
|
|
88
|
+
this.writable = false;
|
|
89
|
+
controller.close();
|
|
90
|
+
},
|
|
91
|
+
}, {
|
|
92
|
+
contextId,
|
|
93
|
+
}, undefined);
|
|
94
|
+
},
|
|
95
|
+
cancel: () => {
|
|
96
|
+
logger.info({ contextId }, 'Stream canceled');
|
|
97
|
+
sseServer.shutdown();
|
|
98
|
+
state.busy = false;
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
return new Response(stream, res);
|
|
102
|
+
});
|
|
103
|
+
serveNodeJs({
|
|
104
|
+
fetch: app.fetch,
|
|
105
|
+
port: config.port || 8080,
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
const getAuthContext = async (authorization, decoder) => {
|
|
109
|
+
if (!authorization || !decoder) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return await decoder(authorization);
|
|
113
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { BedrockAgentCoreClient } from '@aws-sdk/client-bedrock-agentcore';
|
|
2
|
+
import type { CompactionOptions, CompactionResult, Message, MessageStore } from '@looopy-ai/core';
|
|
3
|
+
export interface AgentCoreMemoryMessageStoreConfig {
|
|
4
|
+
memoryId: string;
|
|
5
|
+
agentId: string;
|
|
6
|
+
region?: string;
|
|
7
|
+
client?: BedrockAgentCoreClient;
|
|
8
|
+
extractActorId?: (contextId: string) => string;
|
|
9
|
+
longTermMemoryNamespace?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class AgentCoreMemoryMessageStore implements MessageStore {
|
|
12
|
+
private readonly memoryId;
|
|
13
|
+
private readonly actorId;
|
|
14
|
+
private readonly includeLongTermMemories;
|
|
15
|
+
private readonly longTermMemoryNamespace?;
|
|
16
|
+
private readonly client;
|
|
17
|
+
constructor(config: AgentCoreMemoryMessageStoreConfig);
|
|
18
|
+
append(contextId: string, messages: Message[]): Promise<void>;
|
|
19
|
+
getRecent(contextId: string, options?: {
|
|
20
|
+
maxMessages?: number;
|
|
21
|
+
maxTokens?: number;
|
|
22
|
+
}): Promise<Message[]>;
|
|
23
|
+
getAll(contextId: string): Promise<Message[]>;
|
|
24
|
+
getCount(contextId: string): Promise<number>;
|
|
25
|
+
getRange(contextId: string, startIndex: number, endIndex: number): Promise<Message[]>;
|
|
26
|
+
compact(_contextId: string, _options?: CompactionOptions): Promise<CompactionResult>;
|
|
27
|
+
clear(contextId: string): Promise<void>;
|
|
28
|
+
searchMemories(query: string, options?: {
|
|
29
|
+
maxResults?: number;
|
|
30
|
+
}): Promise<unknown[]>;
|
|
31
|
+
private convertEventsToMessages;
|
|
32
|
+
private retrieveLongTermMemories;
|
|
33
|
+
private formatLongTermMemories;
|
|
34
|
+
private toAgentCoreRole;
|
|
35
|
+
private fromAgentCoreRole;
|
|
36
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { BedrockAgentCoreClient, CreateEventCommand, DeleteEventCommand, ListEventsCommand, RetrieveMemoryRecordsCommand, } from '@aws-sdk/client-bedrock-agentcore';
|
|
2
|
+
import { trimToTokenBudget } from '@looopy-ai/core';
|
|
3
|
+
export class AgentCoreMemoryMessageStore {
|
|
4
|
+
memoryId;
|
|
5
|
+
actorId;
|
|
6
|
+
includeLongTermMemories;
|
|
7
|
+
longTermMemoryNamespace;
|
|
8
|
+
client;
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.memoryId = config.memoryId;
|
|
11
|
+
this.actorId = config.agentId;
|
|
12
|
+
this.includeLongTermMemories = !!config.longTermMemoryNamespace;
|
|
13
|
+
this.longTermMemoryNamespace = config.longTermMemoryNamespace;
|
|
14
|
+
this.client =
|
|
15
|
+
config.client ||
|
|
16
|
+
new BedrockAgentCoreClient({
|
|
17
|
+
region: config.region ?? process.env.AWS_REGION ?? 'us-west-2',
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
async append(contextId, messages) {
|
|
21
|
+
for (const message of messages) {
|
|
22
|
+
const command = new CreateEventCommand({
|
|
23
|
+
memoryId: this.memoryId,
|
|
24
|
+
actorId: this.actorId,
|
|
25
|
+
sessionId: contextId,
|
|
26
|
+
eventTimestamp: new Date(),
|
|
27
|
+
payload: [
|
|
28
|
+
{
|
|
29
|
+
conversational: {
|
|
30
|
+
role: this.toAgentCoreRole(message.role),
|
|
31
|
+
content: { text: message.content },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
blob: {
|
|
36
|
+
toolCallId: message.toolCallId,
|
|
37
|
+
toolCalls: message.toolCalls,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
await this.client.send(command);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async getRecent(contextId, options) {
|
|
46
|
+
const command = new ListEventsCommand({
|
|
47
|
+
memoryId: this.memoryId,
|
|
48
|
+
actorId: this.actorId,
|
|
49
|
+
sessionId: contextId,
|
|
50
|
+
maxResults: options?.maxMessages ?? 50,
|
|
51
|
+
});
|
|
52
|
+
const response = await this.client.send(command);
|
|
53
|
+
const messages = this.convertEventsToMessages(response.events ?? []);
|
|
54
|
+
if (this.includeLongTermMemories && messages.length > 0) {
|
|
55
|
+
const longTerm = await this.retrieveLongTermMemories(this.actorId, 'relevant context');
|
|
56
|
+
if (longTerm.length > 0) {
|
|
57
|
+
messages.unshift({
|
|
58
|
+
role: 'system',
|
|
59
|
+
content: this.formatLongTermMemories(longTerm),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (options?.maxTokens) {
|
|
64
|
+
return trimToTokenBudget(messages, options.maxTokens);
|
|
65
|
+
}
|
|
66
|
+
return messages;
|
|
67
|
+
}
|
|
68
|
+
async getAll(contextId) {
|
|
69
|
+
return this.getRecent(contextId, { maxMessages: 1000 });
|
|
70
|
+
}
|
|
71
|
+
async getCount(contextId) {
|
|
72
|
+
const messages = await this.getRecent(contextId);
|
|
73
|
+
return messages.length;
|
|
74
|
+
}
|
|
75
|
+
async getRange(contextId, startIndex, endIndex) {
|
|
76
|
+
const all = await this.getAll(contextId);
|
|
77
|
+
return all.slice(startIndex, endIndex);
|
|
78
|
+
}
|
|
79
|
+
async compact(_contextId, _options) {
|
|
80
|
+
return {
|
|
81
|
+
summaryMessages: [],
|
|
82
|
+
compactedRange: { start: 0, end: 0 },
|
|
83
|
+
tokensSaved: 0,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async clear(contextId) {
|
|
87
|
+
const list = await this.client.send(new ListEventsCommand({
|
|
88
|
+
memoryId: this.memoryId,
|
|
89
|
+
actorId: this.actorId,
|
|
90
|
+
sessionId: contextId,
|
|
91
|
+
maxResults: 1000,
|
|
92
|
+
}));
|
|
93
|
+
for (const event of list.events ?? []) {
|
|
94
|
+
if (!event.eventId)
|
|
95
|
+
continue;
|
|
96
|
+
await this.client.send(new DeleteEventCommand({
|
|
97
|
+
memoryId: this.memoryId,
|
|
98
|
+
actorId: this.actorId,
|
|
99
|
+
sessionId: contextId,
|
|
100
|
+
eventId: event.eventId,
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async searchMemories(query, options) {
|
|
105
|
+
return this.retrieveLongTermMemories(this.actorId, query, options?.maxResults ?? 10);
|
|
106
|
+
}
|
|
107
|
+
convertEventsToMessages(events) {
|
|
108
|
+
const messages = [];
|
|
109
|
+
events.sort((a, b) => {
|
|
110
|
+
const dateA = a.eventTimestamp?.getTime() ?? 0;
|
|
111
|
+
const dateB = b.eventTimestamp?.getTime() ?? 0;
|
|
112
|
+
return dateA - dateB;
|
|
113
|
+
});
|
|
114
|
+
for (const event of events) {
|
|
115
|
+
const message = { role: 'assistant', content: '' };
|
|
116
|
+
for (const payload of event.payload ?? []) {
|
|
117
|
+
if (payload.conversational) {
|
|
118
|
+
message.role = this.fromAgentCoreRole(payload.conversational.role);
|
|
119
|
+
message.content = payload.conversational.content?.text ?? '';
|
|
120
|
+
}
|
|
121
|
+
const blob = payload.blob;
|
|
122
|
+
if (blob?.toolCallId) {
|
|
123
|
+
message.toolCallId = blob.toolCallId;
|
|
124
|
+
}
|
|
125
|
+
if (blob?.toolCalls) {
|
|
126
|
+
message.toolCalls = blob.toolCalls;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
messages.push(message);
|
|
130
|
+
}
|
|
131
|
+
return messages;
|
|
132
|
+
}
|
|
133
|
+
async retrieveLongTermMemories(_actorId, query, maxResults = 5) {
|
|
134
|
+
const command = new RetrieveMemoryRecordsCommand({
|
|
135
|
+
memoryId: this.memoryId,
|
|
136
|
+
namespace: this.longTermMemoryNamespace,
|
|
137
|
+
searchCriteria: {
|
|
138
|
+
searchQuery: query,
|
|
139
|
+
topK: maxResults,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
const response = await this.client.send(command);
|
|
143
|
+
return response.memoryRecordSummaries ?? [];
|
|
144
|
+
}
|
|
145
|
+
formatLongTermMemories(memories) {
|
|
146
|
+
if (memories.length === 0) {
|
|
147
|
+
return '';
|
|
148
|
+
}
|
|
149
|
+
const lines = memories.map((record) => {
|
|
150
|
+
const data = record;
|
|
151
|
+
return `- ${String(data.content || data.memory || JSON.stringify(record))}`;
|
|
152
|
+
});
|
|
153
|
+
return `Relevant context from previous sessions:\n${lines.join('\n')}`;
|
|
154
|
+
}
|
|
155
|
+
toAgentCoreRole(role) {
|
|
156
|
+
switch (role) {
|
|
157
|
+
case 'user':
|
|
158
|
+
return 'USER';
|
|
159
|
+
case 'assistant':
|
|
160
|
+
return 'ASSISTANT';
|
|
161
|
+
case 'tool':
|
|
162
|
+
return 'TOOL';
|
|
163
|
+
default:
|
|
164
|
+
return 'OTHER';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
fromAgentCoreRole(role) {
|
|
168
|
+
switch (role) {
|
|
169
|
+
case 'USER':
|
|
170
|
+
return 'user';
|
|
171
|
+
case 'ASSISTANT':
|
|
172
|
+
return 'assistant';
|
|
173
|
+
case 'TOOL':
|
|
174
|
+
return 'tool';
|
|
175
|
+
default:
|
|
176
|
+
return 'system';
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type DynamoDBClientConfig } from '@aws-sdk/client-dynamodb';
|
|
2
|
+
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
3
|
+
import type { AgentState, AgentStore } from '@looopy-ai/core';
|
|
4
|
+
export interface DynamoDBAgentStoreConfig {
|
|
5
|
+
tableName: string;
|
|
6
|
+
agentId: string;
|
|
7
|
+
partitionKeyName?: string;
|
|
8
|
+
sortKeyName?: string;
|
|
9
|
+
agentKeyPrefix?: string;
|
|
10
|
+
contextKeyPrefix?: string;
|
|
11
|
+
consistentRead?: boolean;
|
|
12
|
+
entityType?: string;
|
|
13
|
+
documentClient?: DynamoDBDocumentClient;
|
|
14
|
+
dynamoDbClientConfig?: DynamoDBClientConfig;
|
|
15
|
+
}
|
|
16
|
+
export declare class DynamoDBAgentStore implements AgentStore {
|
|
17
|
+
private readonly tableName;
|
|
18
|
+
private readonly agentId;
|
|
19
|
+
private readonly partitionKeyName;
|
|
20
|
+
private readonly sortKeyName;
|
|
21
|
+
private readonly agentKeyPrefix;
|
|
22
|
+
private readonly contextKeyPrefix;
|
|
23
|
+
private readonly consistentRead;
|
|
24
|
+
private readonly entityType;
|
|
25
|
+
private readonly documentClient;
|
|
26
|
+
constructor(config: DynamoDBAgentStoreConfig);
|
|
27
|
+
load(contextId: string): Promise<AgentState | null>;
|
|
28
|
+
save(contextId: string, state: AgentState): Promise<void>;
|
|
29
|
+
delete(contextId: string): Promise<void>;
|
|
30
|
+
private buildKey;
|
|
31
|
+
private serializeState;
|
|
32
|
+
private deserializeState;
|
|
33
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
2
|
+
import { DeleteCommand, DynamoDBDocumentClient, GetCommand, PutCommand, } from '@aws-sdk/lib-dynamodb';
|
|
3
|
+
export class DynamoDBAgentStore {
|
|
4
|
+
tableName;
|
|
5
|
+
agentId;
|
|
6
|
+
partitionKeyName;
|
|
7
|
+
sortKeyName;
|
|
8
|
+
agentKeyPrefix;
|
|
9
|
+
contextKeyPrefix;
|
|
10
|
+
consistentRead;
|
|
11
|
+
entityType;
|
|
12
|
+
documentClient;
|
|
13
|
+
constructor(config) {
|
|
14
|
+
if (!config.tableName) {
|
|
15
|
+
throw new Error('DynamoDBAgentStore requires a tableName');
|
|
16
|
+
}
|
|
17
|
+
if (!config.agentId) {
|
|
18
|
+
throw new Error('DynamoDBAgentStore requires an agentId');
|
|
19
|
+
}
|
|
20
|
+
this.tableName = config.tableName;
|
|
21
|
+
this.agentId = config.agentId;
|
|
22
|
+
this.partitionKeyName = config.partitionKeyName || 'pk';
|
|
23
|
+
this.sortKeyName = config.sortKeyName || 'sk';
|
|
24
|
+
this.agentKeyPrefix = config.agentKeyPrefix || 'agent#';
|
|
25
|
+
this.contextKeyPrefix = config.contextKeyPrefix || 'context#';
|
|
26
|
+
this.consistentRead = config.consistentRead ?? true;
|
|
27
|
+
this.entityType = config.entityType || 'agent-state';
|
|
28
|
+
this.documentClient =
|
|
29
|
+
config.documentClient ||
|
|
30
|
+
DynamoDBDocumentClient.from(new DynamoDBClient(config.dynamoDbClientConfig ?? {}));
|
|
31
|
+
}
|
|
32
|
+
async load(contextId) {
|
|
33
|
+
const command = new GetCommand({
|
|
34
|
+
TableName: this.tableName,
|
|
35
|
+
Key: this.buildKey(contextId),
|
|
36
|
+
ConsistentRead: this.consistentRead,
|
|
37
|
+
});
|
|
38
|
+
const response = await this.documentClient.send(command);
|
|
39
|
+
if (!response.Item) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const item = response.Item;
|
|
43
|
+
if (!item.state) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return this.deserializeState(item.state);
|
|
47
|
+
}
|
|
48
|
+
async save(contextId, state) {
|
|
49
|
+
const serializable = this.serializeState(state);
|
|
50
|
+
const command = new PutCommand({
|
|
51
|
+
TableName: this.tableName,
|
|
52
|
+
Item: {
|
|
53
|
+
...this.buildKey(contextId),
|
|
54
|
+
entityType: this.entityType,
|
|
55
|
+
state: serializable,
|
|
56
|
+
updatedAt: new Date().toISOString(),
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
await this.documentClient.send(command);
|
|
60
|
+
}
|
|
61
|
+
async delete(contextId) {
|
|
62
|
+
const command = new DeleteCommand({
|
|
63
|
+
TableName: this.tableName,
|
|
64
|
+
Key: this.buildKey(contextId),
|
|
65
|
+
});
|
|
66
|
+
await this.documentClient.send(command);
|
|
67
|
+
}
|
|
68
|
+
buildKey(contextId) {
|
|
69
|
+
return {
|
|
70
|
+
[this.partitionKeyName]: `${this.agentKeyPrefix}${this.agentId}`,
|
|
71
|
+
[this.sortKeyName]: `${this.contextKeyPrefix}${contextId}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
serializeState(state) {
|
|
75
|
+
return {
|
|
76
|
+
...state,
|
|
77
|
+
createdAt: state.createdAt.toISOString(),
|
|
78
|
+
lastActivity: state.lastActivity.toISOString(),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
deserializeState(state) {
|
|
82
|
+
return {
|
|
83
|
+
...state,
|
|
84
|
+
createdAt: new Date(state.createdAt),
|
|
85
|
+
lastActivity: new Date(state.lastActivity),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@looopy-ai/aws",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "AWS storage and providers for Looopy AI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json",
|
|
14
|
+
"./ts": "./src/index.ts",
|
|
15
|
+
"./ts/events": "./src/events/index.ts",
|
|
16
|
+
"./ts/observability": "./src/observability/index.ts",
|
|
17
|
+
"./ts/providers": "./src/providers/index.ts",
|
|
18
|
+
"./ts/stores": "./src/stores/index.ts",
|
|
19
|
+
"./ts/server": "./src/server/index.ts",
|
|
20
|
+
"./ts/tools": "./src/tools/index.ts"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"keywords": [
|
|
27
|
+
"agent",
|
|
28
|
+
"ai",
|
|
29
|
+
"rxjs",
|
|
30
|
+
"a2a",
|
|
31
|
+
"llm",
|
|
32
|
+
"aws",
|
|
33
|
+
"bedrock",
|
|
34
|
+
"agentcore"
|
|
35
|
+
],
|
|
36
|
+
"author": "",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@aws-sdk/client-bedrock-agentcore": "^3.932.0",
|
|
40
|
+
"@aws-sdk/client-dynamodb": "^3.932.0",
|
|
41
|
+
"@aws-sdk/lib-dynamodb": "^3.932.0",
|
|
42
|
+
"@hono/node-server": "^1.19.6",
|
|
43
|
+
"@opentelemetry/exporter-metrics-otlp-http": "^0.207.0",
|
|
44
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.207.0",
|
|
45
|
+
"@opentelemetry/instrumentation": "^0.207.0",
|
|
46
|
+
"@opentelemetry/resources": "^2.2.0",
|
|
47
|
+
"@opentelemetry/sdk-metrics": "^2.2.0",
|
|
48
|
+
"@opentelemetry/sdk-trace-base": "^2.2.0",
|
|
49
|
+
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
|
50
|
+
"@opentelemetry/semantic-conventions": "^1.37.0",
|
|
51
|
+
"@smithy/types": "^4.9.0",
|
|
52
|
+
"hono": "^4.10.5",
|
|
53
|
+
"pino-http": "^11.0.0",
|
|
54
|
+
"@looopy-ai/core": "1.0.1"
|
|
55
|
+
},
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"check:exports": "pnpx ts-unused-exports ./tsconfig.json --excludePathsFromReport='index'",
|
|
61
|
+
"check:types": "tsc --noEmit",
|
|
62
|
+
"test": "CI=true vitest",
|
|
63
|
+
"test:watch": "vitest --watch",
|
|
64
|
+
"test:coverage": "vitest --coverage",
|
|
65
|
+
"agentcore": "tsx src/agentcore-runtime-server.ts",
|
|
66
|
+
"build": "tsc -p tsconfig.esm.json",
|
|
67
|
+
"lint": "biome check src",
|
|
68
|
+
"lint:fix": "biome check --write src"
|
|
69
|
+
}
|
|
70
|
+
}
|