@memco/eve 0.0.1 → 0.1.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 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 ADDED
@@ -0,0 +1,267 @@
1
+ # memco for eve
2
+
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="https://github.com/memcoai/eve-memco/blob/main/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 <1`, 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/`](https://github.com/memcoai/eve-memco/tree/main/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](https://github.com/memcoai/eve-memco/blob/main/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,2 @@
1
+ export { memco, type MemcoProviderOptions, type UserNameResolver } from './provider.js';
2
+ export { byTenant, type TenantResolver } from './scope.js';
@@ -0,0 +1,2 @@
1
+ export { memco } from './provider.js';
2
+ export { byTenant } from './scope.js';
@@ -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,13 +1,60 @@
1
1
  {
2
2
  "name": "@memco/eve",
3
- "version": "0.0.1",
4
- "description": "Placeholder for trusted publishing",
5
- "main": "index.js",
3
+ "version": "0.1.1",
4
+ "description": "Memco Shared Memory as a memory provider for eve agents",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/memcoai/eve-memco#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/memcoai/eve-memco.git"
10
+ },
11
+ "bugs": "https://github.com/memcoai/eve-memco/issues",
12
+ "keywords": [
13
+ "memco",
14
+ "eve",
15
+ "shared-memory",
16
+ "memory",
17
+ "agent"
18
+ ],
19
+ "type": "module",
20
+ "engines": {
21
+ "node": ">=24"
22
+ },
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/src/index.d.ts",
26
+ "default": "./dist/src/index.js"
27
+ },
28
+ "./package.json": "./package.json"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "LICENSE",
33
+ "README.md"
34
+ ],
35
+ "workspaces": [
36
+ "examples/single-user",
37
+ "examples/multi-tenant"
38
+ ],
6
39
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
40
+ "typecheck": "tsc -p tsconfig.json --noEmit",
41
+ "lint": "prettier --check . && node-actionlint '.github/workflows/*.yaml'",
42
+ "format": "prettier --write .",
43
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
44
+ "build:test": "rm -rf build && tsc -p tsconfig.json",
45
+ "test": "npm run build:test && node --test --enable-source-maps 'build/tests/**/*.test.js'"
8
46
  },
9
- "keywords": [],
10
- "author": "",
11
- "license": "MIT",
12
- "type": "commonjs"
47
+ "peerDependencies": {
48
+ "eve": ">=0.84.1 <1"
49
+ },
50
+ "devDependencies": {
51
+ "@tktco/node-actionlint": "1.6.0",
52
+ "@types/node": "24.19.2",
53
+ "eve": "0.90.0",
54
+ "prettier": "3.9.6",
55
+ "typescript": "5.9.3"
56
+ },
57
+ "dependencies": {
58
+ "@memco/memcoai": "^0.3.1"
59
+ }
13
60
  }