@memco/eve 0.0.0-stage → 0.1.0
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 +21 -0
- package/README.md +266 -2
- package/dist/src/derive.d.ts +35 -0
- package/dist/src/derive.js +54 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/provider.d.ts +120 -0
- package/dist/src/provider.js +314 -0
- package/dist/src/scope.d.ts +32 -0
- package/dist/src/scope.js +39 -0
- package/package.json +45 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Memco Labs, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,267 @@
|
|
|
1
|
-
#
|
|
1
|
+
# memco for eve
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<p>
|
|
4
|
+
<a href="https://github.com/memcoai/eve-memco/actions/workflows/ci.yaml"><img alt="CI" src="https://github.com/memcoai/eve-memco/actions/workflows/ci.yaml/badge.svg?branch=main"></a>
|
|
5
|
+
<img alt="Node version" src="https://img.shields.io/node/v/@memco/eve">
|
|
6
|
+
<a href="LICENSE"><img alt="Licence" src="https://img.shields.io/npm/l/@memco/eve"></a>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
Shared memory for [eve](https://github.com/vercel/eve) agents. Make useful knowledge from one task
|
|
10
|
+
available to later conversations and other users sharing that memory.
|
|
11
|
+
|
|
12
|
+
`@memco/eve` connects your agent to [memco](https://memco.ai). Agents can search existing knowledge,
|
|
13
|
+
save findings, enrich memories and report whether they helped. For example, an agent can save a
|
|
14
|
+
support workaround that another agent serving the same customer can reuse.
|
|
15
|
+
|
|
16
|
+
Use one API token for a shared memco identity, or an API client to give each tenant its own memory
|
|
17
|
+
network. The provider supplies memory guidance and tools; it does not automatically capture
|
|
18
|
+
conversation transcripts.
|
|
19
|
+
|
|
20
|
+
## Requirements and installation
|
|
21
|
+
|
|
22
|
+
You need Node.js 24 or later, an existing eve app using `eve >=0.84.1 <0.91.0`, and a memco account. If
|
|
23
|
+
you are starting a new app, follow [eve's first-agent tutorial](https://eve.dev/docs/tutorial/first-agent).
|
|
24
|
+
|
|
25
|
+
Install the provider in your eve app:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npm i @memco/eve
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Create an API token in the [memco dashboard](https://spark.memco.ai/) for the shared-identity
|
|
32
|
+
quickstart below. For a multi-tenant app, use an API client with network and user management
|
|
33
|
+
permissions and the admin grant to act for your users; see the
|
|
34
|
+
[credential guide](https://docs.memco.ai/sdk/embedding-memory/#managing-networks-and-users).
|
|
35
|
+
|
|
36
|
+
Keep credentials on the server. Choose one credential set: API client credentials take precedence
|
|
37
|
+
when both they and an API token are set in the environment.
|
|
38
|
+
|
|
39
|
+
## Quickstart: shared identity
|
|
40
|
+
|
|
41
|
+
Every caller of this deployment uses the token's memco identity and shares the memory it can
|
|
42
|
+
access. Use this mode for a personal agent or one trusted audience, not for separate customers.
|
|
43
|
+
|
|
44
|
+
Set the token in your deployment's environment, or in an uncommitted `.env.local` for `eve dev`:
|
|
45
|
+
|
|
46
|
+
```dotenv
|
|
47
|
+
MEMCO_API_TOKEN=replace-with-your-memco-api-token
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Add the memory slot at `agent/memory/memco.ts`:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { memco } from '@memco/eve'
|
|
54
|
+
import { defineMemory } from 'eve/memory'
|
|
55
|
+
import { byPrincipal } from 'eve/memory/scope'
|
|
56
|
+
|
|
57
|
+
export default defineMemory({
|
|
58
|
+
description:
|
|
59
|
+
"The team's shared memory: search it before working something out.",
|
|
60
|
+
provider: memco({ domain: 'knowledge' }),
|
|
61
|
+
namespace: 'acme-support-agent',
|
|
62
|
+
scope: byPrincipal
|
|
63
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`byPrincipal` scopes eve's use of the slot; it does not create separate memco identities or isolate
|
|
67
|
+
callers' stored memories when they use one token. For tenant isolation, use the
|
|
68
|
+
[multi-tenant setup](#multi-tenant-setup) below.
|
|
69
|
+
|
|
70
|
+
### Verify your setup
|
|
71
|
+
|
|
72
|
+
Use a test account or network for this check so the example does not enter production memory.
|
|
73
|
+
Start your agent with tool inputs and results visible:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
npx eve dev --tools full
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
1. Ask: "Save this test fact in memco: the sandbox support demo uses the escalation code
|
|
80
|
+
cobalt-orchid. This is test data, not a real support policy." Confirm that
|
|
81
|
+
`memco__create_memory` runs and its result reports success.
|
|
82
|
+
2. Enter `/reset` in eve's terminal UI to start a fresh session, then ask: "Search memco for the
|
|
83
|
+
sandbox support demo's escalation code." Confirm that `memco__search` returns the saved fact.
|
|
84
|
+
An answer from the model alone is not evidence that memory worked.
|
|
85
|
+
3. After using a real memory on a task, tell the agent whether it worked, and ask it to report
|
|
86
|
+
that through `memco__share_feedback`. That feedback is part of the learning loop, alongside
|
|
87
|
+
creating and enriching memories.
|
|
88
|
+
|
|
89
|
+
The fresh session checks persistence beyond conversation history. See
|
|
90
|
+
[eve's terminal UI guide](https://eve.dev/docs/guides/dev-tui) for tool display and logs. If tools
|
|
91
|
+
are missing, check the active credential, its role and access to the `knowledge` domain, and the
|
|
92
|
+
provider's `[@memco/eve]` logs.
|
|
93
|
+
|
|
94
|
+
## Multi-tenant setup
|
|
95
|
+
|
|
96
|
+
Each tenant gets its own memco network, and each of its users an external user placed in it. Users
|
|
97
|
+
within a tenant share that tenant's memory. The provider derives tenant and user identities from
|
|
98
|
+
verified sign-in, never from the model.
|
|
99
|
+
|
|
100
|
+
Set your API client's credentials in the server environment:
|
|
101
|
+
|
|
102
|
+
```dotenv
|
|
103
|
+
MEMCO_CLIENT_ID=replace-with-your-api-client-id
|
|
104
|
+
MEMCO_CLIENT_SECRET=replace-with-your-api-client-secret
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Use this definition in `agent/memory/memco.ts` instead of the shared-identity example:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { byTenant, memco } from '@memco/eve'
|
|
111
|
+
import { defineMemory } from 'eve/memory'
|
|
112
|
+
|
|
113
|
+
export default defineMemory({
|
|
114
|
+
description:
|
|
115
|
+
"Your organization's shared memory: search it before working something out.",
|
|
116
|
+
provider: memco({ domain: 'knowledge' }),
|
|
117
|
+
namespace: 'acme-support-agent',
|
|
118
|
+
scope: byTenant(principal => {
|
|
119
|
+
const tenant = principal.attributes['tenantId']
|
|
120
|
+
return typeof tenant === 'string' ? tenant : null
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
To show readable names in memco, add `userName: principal => ...` to `memco()`. See
|
|
126
|
+
[Options](#options).
|
|
127
|
+
|
|
128
|
+
Your authentication integration must supply the verified `tenantId` attribute; adapt the resolver
|
|
129
|
+
to your sign-in system. `byTenant` turns memory off, and eve calls no provider, for a caller that is
|
|
130
|
+
not a signed-in user or whose tenant the resolver cannot name. That includes eve's local development
|
|
131
|
+
principal: test this mode through a channel with authenticated users, not the unauthenticated local
|
|
132
|
+
terminal session.
|
|
133
|
+
|
|
134
|
+
A `byTenant` slot needs the API client. With a token instead, every tenant would share one memco
|
|
135
|
+
identity, so the provider fails the turn rather than run.
|
|
136
|
+
|
|
137
|
+
To check sharing and isolation, save a test fact as one user, then search for it in a new session
|
|
138
|
+
as another user in the same tenant. It should be available there. A user in a different tenant must
|
|
139
|
+
not be able to retrieve that tenant's memory, including by its ID with `memco__get_memory`. Inspect
|
|
140
|
+
tool results, not just the agent's answer; a search miss alone does not establish access isolation.
|
|
141
|
+
|
|
142
|
+
Both setups are runnable agents in [`examples/`](examples).
|
|
143
|
+
|
|
144
|
+
## Memory behaviour
|
|
145
|
+
|
|
146
|
+
The provider adds guidance and tools to the agent's normal turn lifecycle:
|
|
147
|
+
|
|
148
|
+
- On the first turn of a memco session, a briefing supplies the domain's guidance (when to search,
|
|
149
|
+
what to save, which tags to use) and the session's instructions. It is recalled once, under the id
|
|
150
|
+
`memco-briefing`, and stays in context.
|
|
151
|
+
- When a session starts and every `nudgeEvery` turns after, a nudge beside the turn's input asks the
|
|
152
|
+
agent to search memory before working and to save lessons the team will need again. Each copy
|
|
153
|
+
stays in context for the rest of the session.
|
|
154
|
+
- The slot exposes `memco__search`, `memco__get_memory`, `memco__create_memory`,
|
|
155
|
+
`memco__enrich_memory`, `memco__share_feedback` and `memco__revert_memory`, as the credential's
|
|
156
|
+
role allows. The `memco__` prefix comes from the slot's file name; tool descriptions and results
|
|
157
|
+
use the same names.
|
|
158
|
+
|
|
159
|
+
The agent decides when to call the tools. Searching is encouraged, not enforced, and writes happen
|
|
160
|
+
only through tool calls, so nothing is saved after a turn or when a session ends; eve gives a
|
|
161
|
+
provider no way to act at either point. Tool inputs, including search queries and memory content, are sent to
|
|
162
|
+
memco; the provider does not automatically upload the conversation transcript. Set your agent's
|
|
163
|
+
instructions to exclude secrets and other content that should not become shared memory.
|
|
164
|
+
|
|
165
|
+
## Options
|
|
166
|
+
|
|
167
|
+
`memco(options)` takes the memco client's own options (`host`, `timeout`, `token`, `clientId`,
|
|
168
|
+
`clientSecret`, ...), each falling back to the environment as the client does, and:
|
|
169
|
+
|
|
170
|
+
| Option | Default | Meaning |
|
|
171
|
+
| --------------- | ----------- | ----------------------------------------------------------------------------- |
|
|
172
|
+
| `domain` | `knowledge` | The memco memory domain the slot reads and writes |
|
|
173
|
+
| `keyTtlMinutes` | `10` | Lifetime, in whole minutes, of the key a multi-tenant turn uses for its user |
|
|
174
|
+
| `userName` | none | Names a multi-tenant user when memco first creates them, from their principal |
|
|
175
|
+
| `nudgeEvery` | `5` | Turns between nudges, after the one a session starts with |
|
|
176
|
+
|
|
177
|
+
`userName` is a label: identity stays the external ID, which holds no personal data, so a name may
|
|
178
|
+
be the first thing that does. An existing user keeps their name. Take it from your verified sign-in,
|
|
179
|
+
never from the model.
|
|
180
|
+
|
|
181
|
+
See the [SDK configuration reference](https://docs.memco.ai/sdk/#configuration) for client
|
|
182
|
+
settings and environment variables.
|
|
183
|
+
|
|
184
|
+
## Production considerations
|
|
185
|
+
|
|
186
|
+
### Namespaces and preview deployments
|
|
187
|
+
|
|
188
|
+
In multi-tenant mode, each tenant's network and users are named from eve's namespace. Set an
|
|
189
|
+
explicit, stable `namespace` before launch:
|
|
190
|
+
|
|
191
|
+
- Make it unique per app within your memco organisation. Apps using the same namespace, tenant ID
|
|
192
|
+
and domain address the same tenant network.
|
|
193
|
+
- Changing it starts tenants in newly named networks without their existing memory. eve's default
|
|
194
|
+
namespace includes the slot's file name, so without an explicit one, renaming the slot does the
|
|
195
|
+
same. This does not delete the old networks.
|
|
196
|
+
- With eve's default namespace, Preview deployments get separate networks and users per branch
|
|
197
|
+
with traffic. Nothing removes those automatically. An explicit namespace is used as-is, with no
|
|
198
|
+
environment suffix: do not reuse the production namespace and organisation for Preview. Use a
|
|
199
|
+
separate memco organisation, or a namespace resolver that returns a distinct preview namespace
|
|
200
|
+
or `null` there. See [eve's namespace rules](https://eve.dev/docs/memory#namespace).
|
|
201
|
+
|
|
202
|
+
A namespace does not partition the token's stored memory in shared-identity mode.
|
|
203
|
+
|
|
204
|
+
### Failure behaviour
|
|
205
|
+
|
|
206
|
+
During recall and tool setup, a memco outage or timeout leaves the turn without memory and the
|
|
207
|
+
provider logs why. Authentication, permission and configuration errors are not treated as outages:
|
|
208
|
+
those errors propagate rather than silently disabling memory. Errors from executing a memory tool
|
|
209
|
+
or closing its session also propagate; the setup fallback does not cover every call in a turn.
|
|
210
|
+
|
|
211
|
+
### API calls and rate limits
|
|
212
|
+
|
|
213
|
+
A multi-tenant turn acts as the tenant's user with a key minted for the turn and ended when it
|
|
214
|
+
completes. eve does not report a failed or cancelled turn to the provider, so that key ends with the
|
|
215
|
+
next turn in the same process, or expires. eve keeps the provider's state per conversation, so a user's first turn in each
|
|
216
|
+
conversation provisions again: creating a user or network that already exists costs a second call
|
|
217
|
+
to find it, and adding a member already in the network changes nothing.
|
|
218
|
+
|
|
219
|
+
| Turn | Calls |
|
|
220
|
+
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
221
|
+
| First of a conversation, multi-tenant | Create (or find) the external user and the tenant network, add the user to it; mint a key; `ListDomains`, `StartSession`, `ListTools`; end the key |
|
|
222
|
+
| Every later one, multi-tenant | Mint a key; `ListDomains`, `GetSession`, `ListTools`; end the key |
|
|
223
|
+
| First of a conversation, shared identity | `ListDomains`, `StartSession`, `ListTools` |
|
|
224
|
+
| Every later one, shared identity | `ListDomains`, `GetSession`, `ListTools` |
|
|
225
|
+
|
|
226
|
+
Each tool the model calls is one more. In multi-tenant mode the API client also exchanges its
|
|
227
|
+
credentials for a token, once and then as it expires. All of a deployment's tenants share the API
|
|
228
|
+
client's rate limits.
|
|
229
|
+
|
|
230
|
+
Deleting a tenant's memory is a call you make yourself, `client.networks.delete(networkId)` with
|
|
231
|
+
the [memco SDK](https://www.npmjs.com/package/@memco/memcoai).
|
|
232
|
+
|
|
233
|
+
## Development
|
|
234
|
+
|
|
235
|
+
From a checkout of this repository, using Node.js 24 or later:
|
|
236
|
+
|
|
237
|
+
```sh
|
|
238
|
+
make install # npm ci
|
|
239
|
+
make check # lint, typecheck, test
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Validation
|
|
243
|
+
|
|
244
|
+
`examples/` holds a shared-identity and a multi-tenant agent, and `examples/validate.ts` drives them
|
|
245
|
+
with real turns against a real model and memco. It checks that memories land, are used and stay
|
|
246
|
+
within their tenant, and that a turn's key is ended. It needs Docker and a dedicated memco test
|
|
247
|
+
organisation: each run writes synthetic facts. A multi-tenant run makes its own tenants and removes
|
|
248
|
+
them at the end; a shared-identity run reverts what it wrote.
|
|
249
|
+
|
|
250
|
+
```sh
|
|
251
|
+
export ANTHROPIC_API_KEY=... MEMCO_API_TOKEN=... MEMCO_CLIENT_ID=... MEMCO_CLIENT_SECRET=...
|
|
252
|
+
make examples # after any change to src/
|
|
253
|
+
make validate MODE=single
|
|
254
|
+
make validate MODE=multi
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`VALIDATE_TURN_SECONDS` sets how long a turn may take, `VALIDATE_SAMPLES=0` skips the scenarios that
|
|
258
|
+
only report rates, and `VALIDATE_KEEP=1` keeps a run's tenants or memories, to look at them in the
|
|
259
|
+
dashboard.
|
|
260
|
+
|
|
261
|
+
`.github/workflows/system.yaml` runs this on every pull request from this repository, and the
|
|
262
|
+
release pipeline requires it before publishing. It reads the same four variables as secrets of a
|
|
263
|
+
GitHub environment named `system-test`.
|
|
264
|
+
|
|
265
|
+
## Licence
|
|
266
|
+
|
|
267
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { SessionAuthContext } from 'eve/context';
|
|
2
|
+
/**
|
|
3
|
+
* The Memco external id of one principal of one tenant.
|
|
4
|
+
*
|
|
5
|
+
* It names the parts eve's `byPrincipal` scope uses, with a missing issuer
|
|
6
|
+
* as `null`, as eve encodes it. The tenant is part of it because an external
|
|
7
|
+
* user sits in one network per domain: without it, a principal under two
|
|
8
|
+
* tenants would be moved between their networks. The namespace is part of it
|
|
9
|
+
* so two apps in one Memco organization never share a user.
|
|
10
|
+
*
|
|
11
|
+
* Changing this derivation is a data migration: every existing user is
|
|
12
|
+
* orphaned.
|
|
13
|
+
*
|
|
14
|
+
* @param namespace The slot's resolved namespace, `ctx.memory.scope.namespace`.
|
|
15
|
+
* @param tenantId The tenant the principal acts under.
|
|
16
|
+
* @param principal The authenticated caller of the turn.
|
|
17
|
+
* @returns `eve-` and 64 hex digits; no personal data.
|
|
18
|
+
*/
|
|
19
|
+
export declare function externalId(namespace: string, tenantId: string, principal: SessionAuthContext): string;
|
|
20
|
+
/**
|
|
21
|
+
* The name of one tenant's Memco network in one domain.
|
|
22
|
+
*
|
|
23
|
+
* Network names are unique across an organization's domains, so the domain
|
|
24
|
+
* is part of the name. The hash keeps long or sensitive tenant ids out of the
|
|
25
|
+
* service and fixes the length within a domain.
|
|
26
|
+
*
|
|
27
|
+
* Changing this derivation is a data migration: every tenant's existing
|
|
28
|
+
* network, and the memory in it, is orphaned.
|
|
29
|
+
*
|
|
30
|
+
* @param namespace The slot's resolved namespace, `ctx.memory.scope.namespace`.
|
|
31
|
+
* @param tenantId The tenant the network serves.
|
|
32
|
+
* @param domain The Memco memory domain.
|
|
33
|
+
* @returns `eve-<domain>-` and 64 hex digits.
|
|
34
|
+
*/
|
|
35
|
+
export declare function networkName(namespace: string, tenantId: string, domain: string): string;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Hash the parts as one JSON array. The array encodes each part with its
|
|
4
|
+
* boundaries, so (`acme1`, `23`) and (`acme`, `123`) never hash alike, as a
|
|
5
|
+
* plain concatenation would.
|
|
6
|
+
*/
|
|
7
|
+
function digest(parts) {
|
|
8
|
+
return createHash('sha256').update(JSON.stringify(parts)).digest('hex');
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The Memco external id of one principal of one tenant.
|
|
12
|
+
*
|
|
13
|
+
* It names the parts eve's `byPrincipal` scope uses, with a missing issuer
|
|
14
|
+
* as `null`, as eve encodes it. The tenant is part of it because an external
|
|
15
|
+
* user sits in one network per domain: without it, a principal under two
|
|
16
|
+
* tenants would be moved between their networks. The namespace is part of it
|
|
17
|
+
* so two apps in one Memco organization never share a user.
|
|
18
|
+
*
|
|
19
|
+
* Changing this derivation is a data migration: every existing user is
|
|
20
|
+
* orphaned.
|
|
21
|
+
*
|
|
22
|
+
* @param namespace The slot's resolved namespace, `ctx.memory.scope.namespace`.
|
|
23
|
+
* @param tenantId The tenant the principal acts under.
|
|
24
|
+
* @param principal The authenticated caller of the turn.
|
|
25
|
+
* @returns `eve-` and 64 hex digits; no personal data.
|
|
26
|
+
*/
|
|
27
|
+
export function externalId(namespace, tenantId, principal) {
|
|
28
|
+
return `eve-${digest([
|
|
29
|
+
namespace,
|
|
30
|
+
tenantId,
|
|
31
|
+
principal.principalType,
|
|
32
|
+
principal.authenticator,
|
|
33
|
+
principal.issuer ?? null,
|
|
34
|
+
principal.principalId
|
|
35
|
+
])}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The name of one tenant's Memco network in one domain.
|
|
39
|
+
*
|
|
40
|
+
* Network names are unique across an organization's domains, so the domain
|
|
41
|
+
* is part of the name. The hash keeps long or sensitive tenant ids out of the
|
|
42
|
+
* service and fixes the length within a domain.
|
|
43
|
+
*
|
|
44
|
+
* Changing this derivation is a data migration: every tenant's existing
|
|
45
|
+
* network, and the memory in it, is orphaned.
|
|
46
|
+
*
|
|
47
|
+
* @param namespace The slot's resolved namespace, `ctx.memory.scope.namespace`.
|
|
48
|
+
* @param tenantId The tenant the network serves.
|
|
49
|
+
* @param domain The Memco memory domain.
|
|
50
|
+
* @returns `eve-<domain>-` and 64 hex digits.
|
|
51
|
+
*/
|
|
52
|
+
export function networkName(namespace, tenantId, domain) {
|
|
53
|
+
return `eve-${domain}-${digest([namespace, tenantId])}`;
|
|
54
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { type AddMemberOptions, type CreateNetworkOptions, type CreateUserOptions, type MemcoOptions, type Session, type SessionOptions } from '@memco/memcoai';
|
|
2
|
+
import type { SessionAuthContext } from 'eve/context';
|
|
3
|
+
import { type MemoryProvider, type MemoryRecallResult, type MemoryToolSet } from 'eve/memory';
|
|
4
|
+
/** Names a user for people reading Memco, or `null` for none. */
|
|
5
|
+
export type UserNameResolver = (principal: SessionAuthContext) => string | null | undefined | Promise<string | null | undefined>;
|
|
6
|
+
/** What the provider sets beyond the Memco client's own options. */
|
|
7
|
+
export interface MemcoProviderOptions extends MemcoOptions {
|
|
8
|
+
/** The Memco memory domain the slot reads and writes. Defaults to `knowledge`. */
|
|
9
|
+
domain?: string;
|
|
10
|
+
/**
|
|
11
|
+
* How many minutes a key acting as a tenant's user lives, in multi-tenant
|
|
12
|
+
* mode. Each turn mints one and ends it when the turn completes, or when the
|
|
13
|
+
* next turn starts after one that failed. A session unused for this long is
|
|
14
|
+
* closed by whichever turn next asks for memory in the process; a session in
|
|
15
|
+
* use outlives it, as its key renews. A process that ends mid-turn leaves its
|
|
16
|
+
* key to expire. Defaults to 10.
|
|
17
|
+
*/
|
|
18
|
+
keyTtlMinutes?: number;
|
|
19
|
+
/**
|
|
20
|
+
* The readable name of a tenant's user, in multi-tenant mode, set when Memco
|
|
21
|
+
* first creates them; an existing user keeps theirs. It is a label: identity
|
|
22
|
+
* stays the external id, which holds no personal data, so a name may be the
|
|
23
|
+
* first thing here that does. Without one, or when it returns none, a user is
|
|
24
|
+
* named by the external id.
|
|
25
|
+
*/
|
|
26
|
+
userName?: UserNameResolver;
|
|
27
|
+
/**
|
|
28
|
+
* How many turns apart the nudge is recalled, after the one a session starts
|
|
29
|
+
* with. Each copy stays in context for the rest of the eve session, so a
|
|
30
|
+
* higher number costs fewer tokens and keeps the instruction further from
|
|
31
|
+
* the latest turn. Defaults to 5.
|
|
32
|
+
*/
|
|
33
|
+
nudgeEvery?: number;
|
|
34
|
+
}
|
|
35
|
+
/** The part of a Memco session the provider uses. */
|
|
36
|
+
export type MemcoSession = Pick<Session, 'id' | 'instructions' | 'domain' | 'close' | 'tools'>;
|
|
37
|
+
/** The part of the Memco client the provider uses. */
|
|
38
|
+
export interface MemcoClient {
|
|
39
|
+
readonly credentialKind: 'token' | 'apiClient';
|
|
40
|
+
readonly memory: {
|
|
41
|
+
startSession(domain: string, options?: SessionOptions): Promise<MemcoSession>;
|
|
42
|
+
resumeSession(sessionId: string, options?: SessionOptions): Promise<MemcoSession>;
|
|
43
|
+
};
|
|
44
|
+
readonly users: {
|
|
45
|
+
createOrGet(externalId: string, options: CreateUserOptions): Promise<{
|
|
46
|
+
readonly id: string;
|
|
47
|
+
}>;
|
|
48
|
+
};
|
|
49
|
+
readonly networks: {
|
|
50
|
+
createOrGet(options: CreateNetworkOptions): Promise<{
|
|
51
|
+
readonly id: string;
|
|
52
|
+
}>;
|
|
53
|
+
addMember(networkId: string, options: AddMemberOptions): Promise<unknown>;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** What eve keeps for one user's Memco session, across turns. JSON only. */
|
|
57
|
+
export interface StoredSession {
|
|
58
|
+
/** The Memco session the next turn resumes. */
|
|
59
|
+
readonly sessionId?: string;
|
|
60
|
+
/** Whether the user and the tenant network are known to exist. */
|
|
61
|
+
readonly provisioned?: boolean;
|
|
62
|
+
}
|
|
63
|
+
/** Every stored session of one eve session, by {@link Identity.stateKey}. */
|
|
64
|
+
export type StoredSessions = Readonly<Record<string, StoredSession>>;
|
|
65
|
+
/** Where the stored sessions live: eve's `defineState` in production. */
|
|
66
|
+
export interface SessionState {
|
|
67
|
+
get(): StoredSessions;
|
|
68
|
+
update(fn: (current: StoredSessions) => StoredSessions): void;
|
|
69
|
+
}
|
|
70
|
+
/** What {@link createProvider} is built from. */
|
|
71
|
+
export interface ProviderDependencies {
|
|
72
|
+
/** The Memco client, created on first use. */
|
|
73
|
+
client: () => MemcoClient;
|
|
74
|
+
state: SessionState;
|
|
75
|
+
domain: string;
|
|
76
|
+
keyTtlMinutes: number;
|
|
77
|
+
nudgeEvery: number;
|
|
78
|
+
userName?: UserNameResolver;
|
|
79
|
+
/** The current time, in milliseconds. */
|
|
80
|
+
now: () => number;
|
|
81
|
+
log: (message: string) => void;
|
|
82
|
+
}
|
|
83
|
+
/** The stable id of the briefing, so a later recall replaces it in place. */
|
|
84
|
+
export declare const BRIEFING_ID = "memco-briefing";
|
|
85
|
+
/**
|
|
86
|
+
* Memco Shared Memory as an eve memory provider.
|
|
87
|
+
*
|
|
88
|
+
* The credential decides the mode. A token is single-user: every caller shares
|
|
89
|
+
* the token's Memco identity. An API client's id and secret are multi-tenant:
|
|
90
|
+
* scope the slot with {@link byTenant}, and each tenant gets its own Memco
|
|
91
|
+
* network, each of its users an external user placed in it.
|
|
92
|
+
*
|
|
93
|
+
* @param options The domain, the key lifetime, and the Memco client's own
|
|
94
|
+
* options, each falling back to the environment as the client does.
|
|
95
|
+
* @returns The provider, for `defineMemory({ provider })`.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```ts
|
|
99
|
+
* export default defineMemory({
|
|
100
|
+
* description: "The team's shared memory.",
|
|
101
|
+
* provider: memco(),
|
|
102
|
+
* scope: byTenant(principal => principal.attributes.tenantId as string)
|
|
103
|
+
* })
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
export declare function memco(options?: MemcoProviderOptions): MemoryProvider;
|
|
107
|
+
/**
|
|
108
|
+
* Build the provider from its dependencies.
|
|
109
|
+
*
|
|
110
|
+
* @internal Exported for the tests, which supply a fake client and state.
|
|
111
|
+
*/
|
|
112
|
+
export declare function createProvider(dependencies: ProviderDependencies): {
|
|
113
|
+
recall: {
|
|
114
|
+
'turn.started': (context: import("eve/memory").MemoryTurnStartedContext) => Promise<MemoryRecallResult>;
|
|
115
|
+
};
|
|
116
|
+
tools: (context: import("eve/memory").MemoryToolsContext) => Promise<MemoryToolSet | null>;
|
|
117
|
+
capture: {
|
|
118
|
+
'turn.completed': (context: import("eve/memory").MemoryTurnCompletedContext) => Promise<void>;
|
|
119
|
+
};
|
|
120
|
+
};
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { Memco, MemcoConfigError, MemcoNotFoundError, MemcoPermissionError, MemcoTimeoutError, MemcoUnavailableError, briefing } from '@memco/memcoai';
|
|
2
|
+
import { defineState } from 'eve/context';
|
|
3
|
+
import { defineMemoryProvider } from 'eve/memory';
|
|
4
|
+
import { defineTool } from 'eve/tools';
|
|
5
|
+
import { externalId, networkName } from './derive.js';
|
|
6
|
+
import { TENANT_SCOPE } from './scope.js';
|
|
7
|
+
/** The stable id of the briefing, so a later recall replaces it in place. */
|
|
8
|
+
export const BRIEFING_ID = 'memco-briefing';
|
|
9
|
+
const sessions = defineState('memco.sessions', () => ({}));
|
|
10
|
+
/**
|
|
11
|
+
* Memco Shared Memory as an eve memory provider.
|
|
12
|
+
*
|
|
13
|
+
* The credential decides the mode. A token is single-user: every caller shares
|
|
14
|
+
* the token's Memco identity. An API client's id and secret are multi-tenant:
|
|
15
|
+
* scope the slot with {@link byTenant}, and each tenant gets its own Memco
|
|
16
|
+
* network, each of its users an external user placed in it.
|
|
17
|
+
*
|
|
18
|
+
* @param options The domain, the key lifetime, and the Memco client's own
|
|
19
|
+
* options, each falling back to the environment as the client does.
|
|
20
|
+
* @returns The provider, for `defineMemory({ provider })`.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* export default defineMemory({
|
|
25
|
+
* description: "The team's shared memory.",
|
|
26
|
+
* provider: memco(),
|
|
27
|
+
* scope: byTenant(principal => principal.attributes.tenantId as string)
|
|
28
|
+
* })
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function memco(options = {}) {
|
|
32
|
+
const { domain = 'knowledge', keyTtlMinutes = 10, nudgeEvery = 5, userName, ...clientOptions } = options;
|
|
33
|
+
// Checked here so a misconfiguration fails when the slot loads, not on
|
|
34
|
+
// every turn.
|
|
35
|
+
if (!Number.isInteger(keyTtlMinutes) || keyTtlMinutes < 1) {
|
|
36
|
+
throw new MemcoConfigError(`keyTtlMinutes must be a whole number of minutes from 1 up, got ${keyTtlMinutes}`);
|
|
37
|
+
}
|
|
38
|
+
if (!Number.isInteger(nudgeEvery) || nudgeEvery < 1) {
|
|
39
|
+
throw new MemcoConfigError(`nudgeEvery must be a whole number of turns from 1 up, got ${nudgeEvery}`);
|
|
40
|
+
}
|
|
41
|
+
let client;
|
|
42
|
+
return defineMemoryProvider(createProvider({
|
|
43
|
+
// Created on first use, so the environment is read when the agent runs
|
|
44
|
+
// rather than when the slot's module loads.
|
|
45
|
+
client: () => (client ??= new Memco(clientOptions)),
|
|
46
|
+
state: sessions,
|
|
47
|
+
domain,
|
|
48
|
+
keyTtlMinutes,
|
|
49
|
+
nudgeEvery,
|
|
50
|
+
...(userName === undefined ? {} : { userName }),
|
|
51
|
+
now: Date.now,
|
|
52
|
+
log: message => console.warn(`[@memco/eve] ${message}`)
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build the provider from its dependencies.
|
|
57
|
+
*
|
|
58
|
+
* @internal Exported for the tests, which supply a fake client and state.
|
|
59
|
+
*/
|
|
60
|
+
export function createProvider(dependencies) {
|
|
61
|
+
const { state, domain, keyTtlMinutes, nudgeEvery, now, log } = dependencies;
|
|
62
|
+
// The session each turn opened, by eve session and identity, held until the
|
|
63
|
+
// turn completes or its key's lifetime passes. A promise, so a rerun or an
|
|
64
|
+
// overlapping call shares the one open rather than minting a second key.
|
|
65
|
+
const open = new Map();
|
|
66
|
+
function identify(scope, auth) {
|
|
67
|
+
const value = typeof scope.value === 'string' ? [scope.value] : scope.value;
|
|
68
|
+
const byTenant = value[0] === TENANT_SCOPE;
|
|
69
|
+
// The domain is in every key, so two slots on one agent never share one.
|
|
70
|
+
if (dependencies.client().credentialKind !== 'apiClient') {
|
|
71
|
+
if (byTenant) {
|
|
72
|
+
throw new Error('byTenant memory needs MEMCO_CLIENT_ID and MEMCO_CLIENT_SECRET: with ' +
|
|
73
|
+
'a token, every tenant would share one Memco identity');
|
|
74
|
+
}
|
|
75
|
+
return { stateKey: `${scope.key}:${domain}` };
|
|
76
|
+
}
|
|
77
|
+
const tenant = value[1];
|
|
78
|
+
const principal = auth.current;
|
|
79
|
+
if (!byTenant ||
|
|
80
|
+
value.length !== 2 ||
|
|
81
|
+
tenant === undefined ||
|
|
82
|
+
principal === null) {
|
|
83
|
+
throw new Error('multi-tenant Memco memory needs a user principal and a scope of one ' +
|
|
84
|
+
'tenant: scope the slot with byTenant');
|
|
85
|
+
}
|
|
86
|
+
const id = externalId(scope.namespace, tenant, principal);
|
|
87
|
+
return {
|
|
88
|
+
stateKey: `${scope.key}:${domain}:${id}`,
|
|
89
|
+
externalId: id,
|
|
90
|
+
networkName: networkName(scope.namespace, tenant, domain),
|
|
91
|
+
principal
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function store(key, entry) {
|
|
95
|
+
state.update(current => ({ ...current, [key]: entry }));
|
|
96
|
+
}
|
|
97
|
+
async function provision(identity, namespace) {
|
|
98
|
+
const client = dependencies.client();
|
|
99
|
+
const id = identity.externalId;
|
|
100
|
+
const name = identity.networkName;
|
|
101
|
+
const given = await dependencies.userName?.(identity.principal);
|
|
102
|
+
const user = await client.users.createOrGet(id, {
|
|
103
|
+
roles: ['reader', 'creator'],
|
|
104
|
+
name: given?.trim() || id
|
|
105
|
+
});
|
|
106
|
+
const network = await client.networks.createOrGet({
|
|
107
|
+
name,
|
|
108
|
+
domain,
|
|
109
|
+
scope: 'customer',
|
|
110
|
+
owner: name,
|
|
111
|
+
description: `Eve memory · namespace: ${namespace} · domain: ${domain}`
|
|
112
|
+
});
|
|
113
|
+
await client.networks.addMember(network.id, { userId: user.id });
|
|
114
|
+
store(identity.stateKey, {
|
|
115
|
+
...state.get()[identity.stateKey],
|
|
116
|
+
provisioned: true
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async function startOrResume(identity, sessionId) {
|
|
120
|
+
const client = dependencies.client();
|
|
121
|
+
const options = identity.externalId === undefined
|
|
122
|
+
? {}
|
|
123
|
+
: { externalId: identity.externalId, keyTtlMinutes };
|
|
124
|
+
if (sessionId !== undefined) {
|
|
125
|
+
try {
|
|
126
|
+
const session = await client.memory.resumeSession(sessionId, options);
|
|
127
|
+
return { session, fresh: false };
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
// The stored session is gone: start another in its place.
|
|
131
|
+
if (!(error instanceof MemcoNotFoundError)) {
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const session = await client.memory.startSession(domain, options);
|
|
137
|
+
store(identity.stateKey, {
|
|
138
|
+
...state.get()[identity.stateKey],
|
|
139
|
+
sessionId: session.id
|
|
140
|
+
});
|
|
141
|
+
return { session, fresh: true };
|
|
142
|
+
}
|
|
143
|
+
async function openFor(identity, namespace) {
|
|
144
|
+
const stored = state.get()[identity.stateKey];
|
|
145
|
+
if (identity.externalId === undefined) {
|
|
146
|
+
return startOrResume(identity, stored?.sessionId);
|
|
147
|
+
}
|
|
148
|
+
const provisioned = stored?.provisioned === true;
|
|
149
|
+
if (!provisioned) {
|
|
150
|
+
await provision(identity, namespace);
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
return await startOrResume(identity, stored?.sessionId);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
// Only a stored `provisioned` can be stale; one just made is not.
|
|
157
|
+
if (!provisioned ||
|
|
158
|
+
(!(error instanceof MemcoNotFoundError) &&
|
|
159
|
+
!(error instanceof MemcoPermissionError))) {
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
// The user or the network was deleted: provision again, once.
|
|
163
|
+
store(identity.stateKey, {});
|
|
164
|
+
await provision(identity, namespace);
|
|
165
|
+
return startOrResume(identity, undefined);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** Close and forget the session `key` holds, if any. */
|
|
169
|
+
async function release(key) {
|
|
170
|
+
const entry = open.get(key);
|
|
171
|
+
if (entry === undefined)
|
|
172
|
+
return;
|
|
173
|
+
open.delete(key);
|
|
174
|
+
// A turn whose open failed has no session to close.
|
|
175
|
+
const turn = await entry.opened.catch(() => null);
|
|
176
|
+
await turn?.session.close();
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Close and forget every session unused for its key's lifetime, in any eve
|
|
180
|
+
* session: a failed turn's is otherwise held until its eve session has
|
|
181
|
+
* another turn. A turn asks for its session before every model call, so this
|
|
182
|
+
* spares a running turn, unless one step outlasts the lifetime. A failure to
|
|
183
|
+
* close is logged, as the turn asking has no part in it.
|
|
184
|
+
*/
|
|
185
|
+
async function evict() {
|
|
186
|
+
const oldest = now() - keyTtlMinutes * 60_000;
|
|
187
|
+
const stale = [...open].filter(([, entry]) => entry.usedAt < oldest);
|
|
188
|
+
await Promise.all(stale.map(([key]) => release(key).catch(error => {
|
|
189
|
+
log(`could not close an expired session: ${error}`);
|
|
190
|
+
})));
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The session for `identity` in this eve session: the one already open, or a
|
|
194
|
+
* new one.
|
|
195
|
+
*
|
|
196
|
+
* @param turnId The turn asking, when known. A session another turn opened
|
|
197
|
+
* is closed first: eve completes only a turn that succeeds, so a failed
|
|
198
|
+
* turn's session is still held, and no session serves two turns. One opened
|
|
199
|
+
* with no turn id, by `tools()`, is adopted: eve can ask for tools before
|
|
200
|
+
* recall, and the session and its `fresh` are this turn's.
|
|
201
|
+
*/
|
|
202
|
+
async function opened(eveSession, identity, namespace, turnId) {
|
|
203
|
+
await evict();
|
|
204
|
+
const key = `${eveSession}\0${identity.stateKey}`;
|
|
205
|
+
const held = open.get(key);
|
|
206
|
+
if (held !== undefined && turnId !== undefined) {
|
|
207
|
+
if (held.turnId === undefined) {
|
|
208
|
+
held.turnId = turnId;
|
|
209
|
+
}
|
|
210
|
+
else if (held.turnId !== turnId) {
|
|
211
|
+
await release(key);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
let entry = open.get(key);
|
|
215
|
+
if (entry === undefined) {
|
|
216
|
+
const created = {
|
|
217
|
+
turnId,
|
|
218
|
+
opened: openFor(identity, namespace),
|
|
219
|
+
usedAt: now()
|
|
220
|
+
};
|
|
221
|
+
entry = created;
|
|
222
|
+
open.set(key, created);
|
|
223
|
+
// A failed open is forgotten, so the next call tries again.
|
|
224
|
+
created.opened.catch(() => {
|
|
225
|
+
if (open.get(key) === created)
|
|
226
|
+
open.delete(key);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
entry.usedAt = now();
|
|
230
|
+
return entry.opened;
|
|
231
|
+
}
|
|
232
|
+
/** Whether a failure is an outage, which leaves the turn without memory. */
|
|
233
|
+
function outage(error, during) {
|
|
234
|
+
if (error instanceof MemcoUnavailableError ||
|
|
235
|
+
error instanceof MemcoTimeoutError) {
|
|
236
|
+
log(`${during} without memory: ${error.message}`);
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
recall: {
|
|
243
|
+
'turn.started': async (context) => {
|
|
244
|
+
const { scope, slot } = context.memory;
|
|
245
|
+
const prefix = `${slot}__`;
|
|
246
|
+
try {
|
|
247
|
+
const identity = identify(scope, context.session.auth);
|
|
248
|
+
const { session, fresh } = await opened(context.session.id, identity, scope.namespace, context.turn.id);
|
|
249
|
+
const messages = [];
|
|
250
|
+
if (fresh) {
|
|
251
|
+
if (session.domain === null) {
|
|
252
|
+
log(`no briefing: the listing does not describe '${domain}'`);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
messages.push({
|
|
256
|
+
id: BRIEFING_ID,
|
|
257
|
+
content: briefing(session.domain, session.instructions, prefix)
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (fresh || context.turn.sequence % nudgeEvery === 0) {
|
|
262
|
+
messages.push({ content: nudge(prefix) });
|
|
263
|
+
}
|
|
264
|
+
return { messages };
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
if (outage(error, 'recall'))
|
|
268
|
+
return null;
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
tools: async (context) => {
|
|
274
|
+
const { scope, slot } = context.memory;
|
|
275
|
+
const prefix = `${slot}__`;
|
|
276
|
+
try {
|
|
277
|
+
const identity = identify(scope, context.session.auth);
|
|
278
|
+
const { session } = await opened(context.session.id, identity, scope.namespace);
|
|
279
|
+
const offered = session.tools({ prefix }).tools;
|
|
280
|
+
return Object.fromEntries(offered.map(tool => [
|
|
281
|
+
tool.name.slice(prefix.length),
|
|
282
|
+
defineTool({
|
|
283
|
+
description: tool.description,
|
|
284
|
+
// Plain JSON Schema, which the SDK's narrower type does not prove.
|
|
285
|
+
inputSchema: tool.parameters,
|
|
286
|
+
execute: input => tool.call(input)
|
|
287
|
+
})
|
|
288
|
+
]));
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
if (outage(error, 'tools'))
|
|
292
|
+
return null;
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
capture: {
|
|
297
|
+
'turn.completed': async (context) => {
|
|
298
|
+
const identity = identify(context.memory.scope, context.session.auth);
|
|
299
|
+
await release(`${context.session.id}\0${identity.stateKey}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* The instruction recalled when a session starts and every `nudgeEvery` turns.
|
|
306
|
+
*
|
|
307
|
+
* @param prefix What the slot's tools start with.
|
|
308
|
+
*/
|
|
309
|
+
function nudge(prefix) {
|
|
310
|
+
return (`Before working on this, search the team's shared memory with ` +
|
|
311
|
+
`${prefix}search for what is already known. When you learn a lesson the ` +
|
|
312
|
+
`team will need again, not general knowledge, save it with ` +
|
|
313
|
+
`${prefix}create_memory or ${prefix}enrich_memory before you finish.`);
|
|
314
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { SessionAuthContext } from 'eve/context';
|
|
2
|
+
import type { MemoryScopeContext } from 'eve/memory';
|
|
3
|
+
/**
|
|
4
|
+
* The first component of every {@link byTenant} scope. It marks the scope as
|
|
5
|
+
* multi-tenant, so a provider holding a token, which would run every tenant as
|
|
6
|
+
* one shared identity, refuses it rather than share memory across tenants.
|
|
7
|
+
*/
|
|
8
|
+
export declare const TENANT_SCOPE = "@memco/eve:tenant";
|
|
9
|
+
/** Names the tenant a user principal acts under, or `null` for none. */
|
|
10
|
+
export type TenantResolver = (principal: SessionAuthContext) => string | null | undefined | Promise<string | null | undefined>;
|
|
11
|
+
/**
|
|
12
|
+
* A multi-tenant scope: every user of one tenant shares that tenant's memory.
|
|
13
|
+
*
|
|
14
|
+
* The scope is the tenant, marked as one with {@link TENANT_SCOPE}, and needs
|
|
15
|
+
* an API client's credentials. Memory is off, and eve calls no provider, for a
|
|
16
|
+
* turn with no principal, a principal that is not a user, or a user whose
|
|
17
|
+
* tenant the resolver cannot name. Never resolve the tenant from anything the
|
|
18
|
+
* model supplies.
|
|
19
|
+
*
|
|
20
|
+
* @param resolveTenant Names the tenant of an authenticated user principal,
|
|
21
|
+
* e.g. from a verified token attribute.
|
|
22
|
+
* @returns A scope resolver for `defineMemory({ scope })`.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* scope: byTenant(principal => {
|
|
27
|
+
* const tenant = principal.attributes.tenantId
|
|
28
|
+
* return typeof tenant === 'string' ? tenant : null
|
|
29
|
+
* })
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare function byTenant(resolveTenant: TenantResolver): (context: MemoryScopeContext) => Promise<readonly [typeof TENANT_SCOPE, string] | null>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The first component of every {@link byTenant} scope. It marks the scope as
|
|
3
|
+
* multi-tenant, so a provider holding a token, which would run every tenant as
|
|
4
|
+
* one shared identity, refuses it rather than share memory across tenants.
|
|
5
|
+
*/
|
|
6
|
+
export const TENANT_SCOPE = '@memco/eve:tenant';
|
|
7
|
+
/**
|
|
8
|
+
* A multi-tenant scope: every user of one tenant shares that tenant's memory.
|
|
9
|
+
*
|
|
10
|
+
* The scope is the tenant, marked as one with {@link TENANT_SCOPE}, and needs
|
|
11
|
+
* an API client's credentials. Memory is off, and eve calls no provider, for a
|
|
12
|
+
* turn with no principal, a principal that is not a user, or a user whose
|
|
13
|
+
* tenant the resolver cannot name. Never resolve the tenant from anything the
|
|
14
|
+
* model supplies.
|
|
15
|
+
*
|
|
16
|
+
* @param resolveTenant Names the tenant of an authenticated user principal,
|
|
17
|
+
* e.g. from a verified token attribute.
|
|
18
|
+
* @returns A scope resolver for `defineMemory({ scope })`.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* scope: byTenant(principal => {
|
|
23
|
+
* const tenant = principal.attributes.tenantId
|
|
24
|
+
* return typeof tenant === 'string' ? tenant : null
|
|
25
|
+
* })
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function byTenant(resolveTenant) {
|
|
29
|
+
return async (context) => {
|
|
30
|
+
const principal = context.session.auth.current;
|
|
31
|
+
if (principal?.principalType !== 'user') {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const tenant = await resolveTenant(principal);
|
|
35
|
+
return typeof tenant === 'string' && tenant !== ''
|
|
36
|
+
? [TENANT_SCOPE, tenant]
|
|
37
|
+
: null;
|
|
38
|
+
};
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memco/eve",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Memco Shared Memory as a memory provider for eve agents",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=24"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/src/index.d.ts",
|
|
13
|
+
"default": "./dist/src/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"LICENSE",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"workspaces": [
|
|
23
|
+
"examples/single-user",
|
|
24
|
+
"examples/multi-tenant"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
28
|
+
"lint": "prettier --check . && node-actionlint '.github/workflows/*.yaml'",
|
|
29
|
+
"format": "prettier --write .",
|
|
30
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
31
|
+
"build:test": "rm -rf build && tsc -p tsconfig.json",
|
|
32
|
+
"test": "npm run build:test && node --test --enable-source-maps 'build/tests/**/*.test.js'"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"eve": ">=0.84.1 <0.91.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@tktco/node-actionlint": "1.6.0",
|
|
39
|
+
"@types/node": "24.19.2",
|
|
40
|
+
"eve": "0.90.0",
|
|
41
|
+
"prettier": "3.9.6",
|
|
42
|
+
"typescript": "5.9.3"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@memco/memcoai": "^0.3.1"
|
|
46
|
+
}
|
|
47
|
+
}
|