@robrain/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 ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
package/README.md CHANGED
@@ -1,3 +1,104 @@
1
- # Temporary Holding Version
1
+ # @robrain/eve
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ RoBrain memory provider for [eve](https://eve.dev) agents. It gives a production agent the same memory your coding agents use: **the team's engineering decisions, including the alternatives you rejected and why** — so the agent stops re-proposing approaches your team already ruled out.
4
+
5
+ Other eve memory providers remember things about each *user* of the agent. RoBrain remembers what the *team that builds and runs the agent* decided. Because it's the same store your Claude Code, Cursor, and Codex sessions write to, a rejection recorded while building the agent reaches it in production, and a decision made while operating it reaches the engineer's next coding session.
6
+
7
+ ## What the provider does
8
+
9
+ eve drives every memory provider through one lifecycle. RoBrain fills it like this:
10
+
11
+ | eve hook | RoBrain behavior |
12
+ |---|---|
13
+ | `recall` at turn start | Injects the project's always-on summary (top decisions with their rejected alternatives) and, for the incoming message, up to three **veto warnings** — prior decisions whose rejected options the message is drifting toward. Two-tier: deterministic exact match on rejected options, then semantic search. |
14
+ | `recall` after compaction | Re-injects the summary — the moment compaction would otherwise have dropped it. |
15
+ | `capture` at turn end | Ships the completed turn for server-side decision extraction. RoBrain extracts decisions *and their rejections* as structured fields and dedups near-duplicates; the provider stays a dumb pipe. |
16
+ | tools | `<slot>__check_prior_decisions` — lets the model check decision memory before committing to an approach. |
17
+
18
+ Recalled messages carry stable ids (`robrain:summary`, `robrain:decision:<id>`), so eve supersedes them in place instead of accumulating copies.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ npm install @robrain/eve
24
+ ```
25
+
26
+ (Once listed in eve's registry: `eve add memory/robrain`.)
27
+
28
+ Declare a slot. The scope is **shared**, not per-user — this memory belongs to the team:
29
+
30
+ ```ts
31
+ // agent/memory/robrain.ts
32
+ import { robrainMemory } from "@robrain/eve";
33
+ import { defineMemory } from "eve/memory";
34
+
35
+ export default defineMemory({
36
+ description: "The team's engineering decisions, including rejected alternatives and why.",
37
+ provider: robrainMemory({
38
+ apiKey: () => process.env.ROBRAIN_API_KEY,
39
+ }),
40
+ scope: "team",
41
+ });
42
+ ```
43
+
44
+ Pass `apiKey` as a function so it's read when the agent runs, not when `eve build` loads the slot.
45
+
46
+ Add to `agent/instructions.md` so the model uses the tool before proposing approaches:
47
+
48
+ ```md
49
+ Before proposing any technology, architecture, or approach, call
50
+ `robrain__check_prior_decisions` with a short description of the task. Do not
51
+ re-propose a rejected approach without flagging the prior rejection and why
52
+ circumstances changed.
53
+ ```
54
+
55
+ ## Connect it to RoBrain
56
+
57
+ The provider talks to the RoBrain API. Point it at either:
58
+
59
+ - **Rory Plans cloud** (hosted; reachable from deployed agents on Vercel): set `ROBRAIN_API_URL` to your Rory Plans API URL and `ROBRAIN_API_KEY` to an org API key (`rp_live_…`). On cloud, relevance comes from `POST /context` — planning-ranked, token-budgeted decisions plus server-side veto warnings, recorded in the injection ledger so effectiveness can be measured per session — in addition to the deterministic veto scan.
60
+ - **Self-hosted RoBrain** (open source): `ROBRAIN_API_URL=http://127.0.0.1:3001` for `eve dev` (the default), or the public URL of your own Perception for deployed agents. `PERCEPTION_API_URL` / `PERCEPTION_API_KEY` are honored as fallbacks, so an existing self-hosted setup needs no new variables. Relevance uses `GET /decisions?query=` (similarity-gated); the provider detects the missing `/context` route once and falls back automatically.
61
+
62
+ Either way, set `ROBRAIN_PROJECT_ID` to the project whose memory this agent shares (the same id your coding-agent integrations use — that's what makes the memory combined). Start a self-hosted stack with `npx robrain@latest up && npx robrain init-project`.
63
+
64
+ | Option | Env fallback | Default |
65
+ |---|---|---|
66
+ | `apiUrl` | `ROBRAIN_API_URL`, `PERCEPTION_API_URL` | `http://127.0.0.1:3001` |
67
+ | `apiKey` | `ROBRAIN_API_KEY`, `PERCEPTION_API_KEY` | — |
68
+ | `projectId` | `ROBRAIN_PROJECT_ID` | — (slot is inactive without one) |
69
+ | `summary` | | `true` |
70
+ | `vetoLimit` | | `3` |
71
+ | `capture` | | `true` |
72
+ | `timeoutMs` | | `2500` |
73
+
74
+ ### Multi-tenant apps
75
+
76
+ `projectId` also accepts a resolver that maps eve's memory scope to a RoBrain project, for apps where different scopes should read different teams' memory:
77
+
78
+ ```ts
79
+ provider: robrainMemory({
80
+ projectId: ({ memory }) => tenantToProject[String(memory.scope.value)] ?? null,
81
+ }),
82
+ scope: (ctx) => ctx.session.auth.current?.attributes.tenantId ?? null,
83
+ ```
84
+
85
+ Returning `null` leaves the slot inactive for that operation.
86
+
87
+ ## Cloud-specific behavior
88
+
89
+ - Veto warnings from `/context` arrive one per rejected option with a server-rendered line; the provider groups them per decision and recalls those lines verbatim.
90
+ - Decisions that were **captured but not yet reviewed** still produce veto warnings, and the cloud attaches a `required_acknowledgement`. The provider recalls it as its own message (`robrain:acknowledgement`) so the agent surfaces the warning and gets an explicit acknowledgement before proceeding against the option — the cloud's pre-task veto acknowledgement, carried into eve.
91
+ - Only **reviewed** decisions are eligible as ranked memories; unreviewed ones surface via warnings only.
92
+ - Turn sequences sent to the cloud are 1-based (eve counts from 0), matching how capture numbers turns.
93
+ - Once `/context` has succeeded for an API URL, a transient failure recalls no semantic vetoes for that turn rather than falling back to the unranked list endpoint.
94
+
95
+ ## Behavior under failure
96
+
97
+ eve fails a turn if a recall handler throws, so this provider never throws: an unreachable or misconfigured API means "recall nothing" and "capture nothing," and the agent continues without memory. Requests carry hard timeouts (`timeoutMs`; recall runs before the model call, so keep it short). eve delivers capture at least once; the provider skips repeated `operationId`s in-process, and RoBrain dedups near-identical decisions server-side.
98
+
99
+ ## Notes
100
+
101
+ - Requires Node ≥ 24 (eve's floor). Verified against eve 0.82.0.
102
+ - Recalled content enters model context as user-role messages attributed to the slot, per eve's trust model — never as system instructions. (That placement is the one RoBrain's public benchmark, [VetoBench](../vetobench/README.md), measures; with rejections in context, 0 re-proposals in 159 of 159 tasks across six models.)
103
+ - The provider partitions by RoBrain project; a slot bound to one `projectId` shares memory across all scopes that reach it. Use the resolver form when scopes must not share.
104
+ - Prefer this package over the raw-surfaces integration in [plugins/eve](../../plugins/eve/README.md), which predates eve's memory subsystem and remains as a reference.
@@ -0,0 +1,75 @@
1
+ import type { ResolvedConnection } from "./config.js";
2
+ export interface RejectedOption {
3
+ readonly option: string;
4
+ readonly reason: string;
5
+ }
6
+ export interface VetoDecision {
7
+ readonly id?: string;
8
+ readonly decision: string;
9
+ readonly rationale?: string;
10
+ readonly similarity?: number;
11
+ readonly exact?: boolean;
12
+ readonly rejected: readonly RejectedOption[];
13
+ /** Cloud /context ships injection-ready warning lines; prefer them when present. */
14
+ readonly messages?: readonly string[];
15
+ /** false when the decision was captured but not yet reviewed (cloud). */
16
+ readonly reviewed?: boolean;
17
+ }
18
+ /** A planning-ranked decision from cloud POST /context. */
19
+ export interface ContextMemory {
20
+ readonly id: string;
21
+ readonly decision: string;
22
+ readonly rationale?: string;
23
+ readonly rejected: readonly RejectedOption[];
24
+ readonly score?: number;
25
+ }
26
+ export interface ContextRecall {
27
+ readonly memories: readonly ContextMemory[];
28
+ readonly vetoes: readonly VetoDecision[];
29
+ /** Cloud may require an explicit acknowledgement before a vetoed path is taken. */
30
+ readonly requiredAcknowledgement?: string;
31
+ }
32
+ export interface CapturedTurn {
33
+ readonly sessionId: string;
34
+ readonly sequence: number;
35
+ readonly userMessage: string;
36
+ readonly reply: string;
37
+ }
38
+ export declare class RobrainClient {
39
+ private readonly connection;
40
+ constructor(connection: ResolvedConnection);
41
+ private request;
42
+ fetchJson(path: string, init?: RequestInit, timeoutMs?: number): Promise<any | null>;
43
+ /** Deterministic tier: POST /veto-scan — exact rejected-option matches, no embeddings. */
44
+ exactVetoes(projectId: string, text: string): Promise<VetoDecision[]>;
45
+ /** Self-hosted semantic tier: GET /decisions?query=, similarity-gated. */
46
+ semanticVetoes(projectId: string, text: string, limit?: number): Promise<VetoDecision[]>;
47
+ /**
48
+ * Cloud semantic tier: POST /context. Returns null when the API does not
49
+ * offer the route (self-hosted) or the call failed; the caller falls back to
50
+ * semanticVetoes(). A 404 is remembered per API URL so self-hosted setups
51
+ * pay for the probe once.
52
+ */
53
+ contextRecall(projectId: string, text: string, options?: {
54
+ sessionId?: string;
55
+ sequence?: number;
56
+ budgetTokens?: number;
57
+ }): Promise<ContextRecall | null>;
58
+ /** The project's always-on summary: top decisions with their rejected alternatives. */
59
+ alwaysOnSummary(projectId: string): Promise<string | null>;
60
+ /**
61
+ * Ship a completed turn for server-side decision extraction. RoBrain
62
+ * classifies, extracts rejected[] and dedups near-duplicates on its side, so
63
+ * this stays a dumb pipe. Returns true when the request was accepted.
64
+ */
65
+ captureTurn(projectId: string, turn: CapturedTurn): Promise<boolean>;
66
+ }
67
+ /** Merge exact matches ahead of semantic ones, deduping by decision id. */
68
+ export declare function mergeVetoes(exact: readonly VetoDecision[], semantic: readonly VetoDecision[]): VetoDecision[];
69
+ /**
70
+ * Whether POST /context has been confirmed for an API URL: true after a
71
+ * success, false after a 404, undefined until probed.
72
+ */
73
+ export declare function isContextSupported(apiUrl: string): boolean | undefined;
74
+ /** Reset the per-URL /context probe memo (tests). */
75
+ export declare function resetContextSupport(): void;
package/dist/client.js ADDED
@@ -0,0 +1,192 @@
1
+ // Thin client for the RoBrain API (self-hosted Perception or Rory Plans cloud —
2
+ // same auth, mostly the same routes). Fail-open by construction: every method
3
+ // returns null/[] on any failure and never throws, because a throwing recall
4
+ // handler fails the user's turn in eve.
5
+ //
6
+ // One route differs by tier. Semantic recall is GET /decisions?query= on
7
+ // self-hosted Perception, and POST /context on Rory cloud (planning-ranked,
8
+ // token-budgeted, with server-side veto warnings and an injection ledger).
9
+ // Cloud's GET /decisions is a plain list. The client probes /context once per
10
+ // API URL and remembers the answer.
11
+ /** Similarity gate for the self-hosted semantic tier; exact /veto-scan matches are exempt. */
12
+ const MIN_SIMILARITY = 0.45;
13
+ /** Per API URL: does POST /context exist? undefined = not probed yet. */
14
+ const contextSupport = new Map();
15
+ export class RobrainClient {
16
+ connection;
17
+ constructor(connection) {
18
+ this.connection = connection;
19
+ }
20
+ async request(path, init = {}, timeoutMs) {
21
+ const { apiUrl, apiKey } = this.connection;
22
+ try {
23
+ const res = await fetch(`${apiUrl}${path}`, {
24
+ ...init,
25
+ headers: {
26
+ "Content-Type": "application/json",
27
+ ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
28
+ ...(init.headers ?? {}),
29
+ },
30
+ signal: AbortSignal.timeout(timeoutMs ?? this.connection.timeoutMs),
31
+ });
32
+ const body = await res.json().catch(() => null);
33
+ return { status: res.status, body };
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ async fetchJson(path, init = {}, timeoutMs) {
40
+ const res = await this.request(path, init, timeoutMs);
41
+ return res && res.status >= 200 && res.status < 300 ? res.body : null;
42
+ }
43
+ /** Deterministic tier: POST /veto-scan — exact rejected-option matches, no embeddings. */
44
+ async exactVetoes(projectId, text) {
45
+ const scan = await this.fetchJson("/veto-scan", { method: "POST", body: JSON.stringify({ project_id: projectId, text }) }, 1500);
46
+ return (Array.isArray(scan?.matches) ? scan.matches : []).map((m) => ({ ...m, exact: true }));
47
+ }
48
+ /** Self-hosted semantic tier: GET /decisions?query=, similarity-gated. */
49
+ async semanticVetoes(projectId, text, limit = 8) {
50
+ const data = await this.fetchJson(`/decisions?${new URLSearchParams({ project_id: projectId, query: text, limit: String(limit) })}`);
51
+ const decisions = Array.isArray(data) ? data : data?.decisions;
52
+ if (!Array.isArray(decisions))
53
+ return [];
54
+ return decisions
55
+ .filter((d) => Array.isArray(d.rejected) && d.rejected.length > 0)
56
+ .filter((d) => typeof d.similarity !== "number" || d.similarity >= MIN_SIMILARITY)
57
+ .sort((a, b) => (b.similarity ?? 0) - (a.similarity ?? 0));
58
+ }
59
+ /**
60
+ * Cloud semantic tier: POST /context. Returns null when the API does not
61
+ * offer the route (self-hosted) or the call failed; the caller falls back to
62
+ * semanticVetoes(). A 404 is remembered per API URL so self-hosted setups
63
+ * pay for the probe once.
64
+ */
65
+ async contextRecall(projectId, text, options = {}) {
66
+ const { apiUrl } = this.connection;
67
+ if (contextSupport.get(apiUrl) === false)
68
+ return null;
69
+ const res = await this.request("/context", {
70
+ method: "POST",
71
+ body: JSON.stringify({
72
+ project_id: projectId,
73
+ task_description: text,
74
+ ...(options.sessionId ? { session_id: options.sessionId } : {}),
75
+ ...(typeof options.sequence === "number" ? { sequence: options.sequence } : {}),
76
+ budget_tokens: options.budgetTokens ?? 1500,
77
+ }),
78
+ }, 3000);
79
+ if (!res)
80
+ return null;
81
+ if (res.status === 404) {
82
+ contextSupport.set(apiUrl, false);
83
+ return null;
84
+ }
85
+ if (res.status < 200 || res.status >= 300 || !res.body)
86
+ return null;
87
+ contextSupport.set(apiUrl, true);
88
+ const body = res.body;
89
+ const memories = (Array.isArray(body.memories) ? body.memories : [])
90
+ .filter((m) => m && typeof m.id === "string" && typeof m.decision === "string")
91
+ .map((m) => ({
92
+ id: m.id,
93
+ decision: m.decision,
94
+ ...(typeof m.rationale === "string" ? { rationale: m.rationale } : {}),
95
+ rejected: Array.isArray(m.rejected) ? m.rejected : [],
96
+ ...(typeof m.score === "number" ? { score: m.score } : {}),
97
+ }));
98
+ // Cloud warnings are one entry per rejected option:
99
+ // { decision_id, rejected_option, reason, message, reviewed }
100
+ // Group them back into one VetoDecision per decision. The decision text
101
+ // itself is not part of a warning; the server-rendered `message` lines are.
102
+ const grouped = new Map();
103
+ for (const w of Array.isArray(body.veto_warnings) ? body.veto_warnings : []) {
104
+ if (!w || typeof w.decision_id !== "string" || typeof w.rejected_option !== "string")
105
+ continue;
106
+ const entry = grouped.get(w.decision_id) ?? { rejected: [], messages: [], reviewed: true };
107
+ entry.rejected.push({ option: w.rejected_option, reason: typeof w.reason === "string" ? w.reason : "" });
108
+ if (typeof w.message === "string" && w.message.trim())
109
+ entry.messages.push(w.message.trim());
110
+ if (w.reviewed === false)
111
+ entry.reviewed = false;
112
+ grouped.set(w.decision_id, entry);
113
+ }
114
+ const vetoes = [...grouped.entries()].map(([id, entry]) => ({
115
+ id,
116
+ decision: entry.messages[0] ?? `Prior decision ${id}`,
117
+ rejected: entry.rejected,
118
+ messages: entry.messages,
119
+ reviewed: entry.reviewed,
120
+ }));
121
+ const ack = body.required_acknowledgement;
122
+ const requiredAcknowledgement = typeof ack === "string"
123
+ ? ack
124
+ : ack && typeof ack === "object" && typeof (ack.message ?? ack.text) === "string"
125
+ ? String(ack.message ?? ack.text)
126
+ : undefined;
127
+ return { memories, vetoes, ...(requiredAcknowledgement ? { requiredAcknowledgement } : {}) };
128
+ }
129
+ /** The project's always-on summary: top decisions with their rejected alternatives. */
130
+ async alwaysOnSummary(projectId) {
131
+ if (!projectId)
132
+ return null;
133
+ const data = await this.fetchJson(`/projects/${encodeURIComponent(projectId)}/summary`, {}, 3000);
134
+ const text = data?.summary ?? data?.always_on_summary;
135
+ return typeof text === "string" && text.trim() ? text.trim() : null;
136
+ }
137
+ /**
138
+ * Ship a completed turn for server-side decision extraction. RoBrain
139
+ * classifies, extracts rejected[] and dedups near-duplicates on its side, so
140
+ * this stays a dumb pipe. Returns true when the request was accepted.
141
+ */
142
+ async captureTurn(projectId, turn) {
143
+ if (!projectId)
144
+ return false;
145
+ const res = await this.fetchJson("/signals", {
146
+ method: "POST",
147
+ headers: { "X-Project-Id": projectId },
148
+ body: JSON.stringify({
149
+ signal: {
150
+ turn: {
151
+ session_id: turn.sessionId.slice(0, 200),
152
+ sequence: turn.sequence,
153
+ user_message: turn.userMessage.slice(0, 60_000),
154
+ claude_reply: turn.reply.slice(0, 60_000),
155
+ files_touched: [],
156
+ timestamp: new Date().toISOString(),
157
+ },
158
+ decision_type: "unclassified",
159
+ confidence: 0,
160
+ files_affected: [],
161
+ scope: "team",
162
+ needs_classification: true,
163
+ },
164
+ }),
165
+ }, 8000);
166
+ return res !== null;
167
+ }
168
+ }
169
+ /** Merge exact matches ahead of semantic ones, deduping by decision id. */
170
+ export function mergeVetoes(exact, semantic) {
171
+ const seen = new Set();
172
+ const merged = [];
173
+ for (const d of [...exact, ...semantic]) {
174
+ const key = d.id ?? d.decision;
175
+ if (!key || seen.has(key))
176
+ continue;
177
+ seen.add(key);
178
+ merged.push(d);
179
+ }
180
+ return merged;
181
+ }
182
+ /**
183
+ * Whether POST /context has been confirmed for an API URL: true after a
184
+ * success, false after a 404, undefined until probed.
185
+ */
186
+ export function isContextSupported(apiUrl) {
187
+ return contextSupport.get(apiUrl);
188
+ }
189
+ /** Reset the per-URL /context probe memo (tests). */
190
+ export function resetContextSupport() {
191
+ contextSupport.clear();
192
+ }
@@ -0,0 +1,53 @@
1
+ export type Lazy<T> = T | (() => T);
2
+ /** The subset of eve's operation context a project resolver may use. */
3
+ export interface ProjectResolverContext {
4
+ readonly memory: {
5
+ readonly scope: {
6
+ readonly key: string;
7
+ readonly namespace: string;
8
+ readonly value: string | readonly string[];
9
+ };
10
+ readonly slot: string;
11
+ };
12
+ }
13
+ export interface RobrainMemoryOptions {
14
+ /** API key. Default: ROBRAIN_API_KEY, then PERCEPTION_API_KEY. */
15
+ readonly apiKey?: Lazy<string | undefined>;
16
+ /**
17
+ * Base URL of the RoBrain API — a self-hosted Perception or the Rory Plans
18
+ * cloud API. Default: ROBRAIN_API_URL, then PERCEPTION_API_URL, then
19
+ * http://127.0.0.1:3001 (the self-hosted Docker stack).
20
+ */
21
+ readonly apiUrl?: Lazy<string | undefined>;
22
+ /**
23
+ * RoBrain project whose decision memory this slot reads and writes. A
24
+ * string binds the slot to one project (the common case: one eve app, one
25
+ * engineering team). A resolver maps eve's memory scope to a project for
26
+ * multi-tenant apps. Default: ROBRAIN_PROJECT_ID.
27
+ */
28
+ readonly projectId?: string | ((context: ProjectResolverContext) => string | null | Promise<string | null>);
29
+ /** Recall the always-on project summary (top decisions + vetoes). Default true. */
30
+ readonly summary?: boolean;
31
+ /** Max veto warnings recalled per turn. Default 3. */
32
+ readonly vetoLimit?: number;
33
+ /**
34
+ * On Rory Plans cloud, use POST /context for relevance: planning-ranked,
35
+ * token-budgeted decisions plus server-side veto warnings, recorded in the
36
+ * injection ledger. Self-hosted Perception has no /context and falls back to
37
+ * GET /decisions?query= automatically. Default true.
38
+ */
39
+ readonly context?: boolean;
40
+ /** Token budget passed to cloud /context for ranked decisions. Default 1500. */
41
+ readonly contextBudgetTokens?: number;
42
+ /** Capture completed turns for server-side decision extraction. Default true. */
43
+ readonly capture?: boolean;
44
+ /** Per-request timeout. Recall runs before the model call, so keep it short. Default 2500. */
45
+ readonly timeoutMs?: number;
46
+ }
47
+ export interface ResolvedConnection {
48
+ readonly apiUrl: string;
49
+ readonly apiKey: string;
50
+ readonly timeoutMs: number;
51
+ }
52
+ export declare function resolveConnection(options: RobrainMemoryOptions, env?: NodeJS.ProcessEnv): ResolvedConnection;
53
+ export declare function resolveProjectId(options: RobrainMemoryOptions, context: ProjectResolverContext, env?: NodeJS.ProcessEnv): Promise<string | null>;
package/dist/config.js ADDED
@@ -0,0 +1,52 @@
1
+ // Option resolution for the RoBrain eve memory provider.
2
+ //
3
+ // Everything is read lazily, at the moment a handler runs — never when
4
+ // `eve build` loads the memory slot. (A key captured at build time has to exist
5
+ // in the build environment too, and stale values bake into the artifact.)
6
+ function unwrap(value) {
7
+ return typeof value === "function" ? value() : value;
8
+ }
9
+ // Node 17+ resolves `localhost` IPv6-first (::1) while the Docker stack only
10
+ // binds 127.0.0.1; rewrite so old configs keep working.
11
+ function normalizeLoopback(url) {
12
+ try {
13
+ const u = new URL(url);
14
+ if (u.hostname === "localhost") {
15
+ u.hostname = "127.0.0.1";
16
+ return u.toString().replace(/\/$/, "");
17
+ }
18
+ }
19
+ catch {
20
+ // not a URL we can parse — leave as-is and let fetch report it
21
+ }
22
+ return url.replace(/\/$/, "");
23
+ }
24
+ export function resolveConnection(options, env = process.env) {
25
+ const apiUrl = unwrap(options.apiUrl)?.trim() ||
26
+ env.ROBRAIN_API_URL?.trim() ||
27
+ env.PERCEPTION_API_URL?.trim() ||
28
+ "http://127.0.0.1:3001";
29
+ const apiKey = unwrap(options.apiKey)?.trim() ||
30
+ env.ROBRAIN_API_KEY?.trim() ||
31
+ env.PERCEPTION_API_KEY?.trim() ||
32
+ "";
33
+ return {
34
+ apiUrl: normalizeLoopback(apiUrl),
35
+ apiKey,
36
+ timeoutMs: options.timeoutMs ?? 2500,
37
+ };
38
+ }
39
+ export async function resolveProjectId(options, context, env = process.env) {
40
+ const configured = options.projectId;
41
+ if (typeof configured === "function") {
42
+ try {
43
+ const id = await configured(context);
44
+ return typeof id === "string" && id.trim() ? id.trim() : null;
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ }
50
+ const id = configured?.trim() || env.ROBRAIN_PROJECT_ID?.trim() || "";
51
+ return id || null;
52
+ }
@@ -0,0 +1,3 @@
1
+ export { robrainMemory } from "./provider.js";
2
+ export type { RobrainMemoryOptions, ProjectResolverContext } from "./config.js";
3
+ export type { VetoDecision, RejectedOption, ContextMemory } from "./client.js";
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ // @robrain/eve — RoBrain memory provider for eve agents.
2
+ //
3
+ // import { robrainMemory } from "@robrain/eve";
4
+ // import { defineMemory } from "eve/memory";
5
+ //
6
+ // export default defineMemory({
7
+ // description: "The team's engineering decisions, including rejected alternatives.",
8
+ // provider: robrainMemory({ apiKey: () => process.env.ROBRAIN_API_KEY }),
9
+ // scope: "team",
10
+ // });
11
+ export { robrainMemory } from "./provider.js";
@@ -0,0 +1,8 @@
1
+ export interface MessageLike {
2
+ readonly role: string;
3
+ readonly content: unknown;
4
+ }
5
+ /** Flatten a ModelMessage content value to plain text (text parts only). */
6
+ export declare function textOf(content: unknown): string;
7
+ /** Text of the last message with the given role, or "" when there is none. */
8
+ export declare function lastTextByRole(messages: readonly MessageLike[], role: string): string;
@@ -0,0 +1,32 @@
1
+ // Text extraction over the AI SDK's ModelMessage shape, typed structurally so
2
+ // the provider carries no dependency on a specific `ai` version.
3
+ /** Flatten a ModelMessage content value to plain text (text parts only). */
4
+ export function textOf(content) {
5
+ if (typeof content === "string")
6
+ return content;
7
+ if (Array.isArray(content)) {
8
+ return content
9
+ .map((part) => {
10
+ if (part && typeof part === "object" && part.type === "text") {
11
+ const text = part.text;
12
+ return typeof text === "string" ? text : "";
13
+ }
14
+ return "";
15
+ })
16
+ .filter(Boolean)
17
+ .join("\n");
18
+ }
19
+ return "";
20
+ }
21
+ /** Text of the last message with the given role, or "" when there is none. */
22
+ export function lastTextByRole(messages, role) {
23
+ for (let i = messages.length - 1; i >= 0; i--) {
24
+ const m = messages[i];
25
+ if (m && m.role === role) {
26
+ const text = textOf(m.content).trim();
27
+ if (text)
28
+ return text;
29
+ }
30
+ }
31
+ return "";
32
+ }
@@ -0,0 +1,11 @@
1
+ import type { MemoryProvider } from "eve/memory";
2
+ import { type ContextMemory, type VetoDecision } from "./client.js";
3
+ import { type RobrainMemoryOptions } from "./config.js";
4
+ export declare function renderSummary(summary: string): string;
5
+ export declare function renderVeto(decision: VetoDecision): string;
6
+ export declare function renderMemory(memory: ContextMemory): string;
7
+ export declare function decisionMessageId(decision: {
8
+ id?: string;
9
+ decision: string;
10
+ }): string;
11
+ export declare function robrainMemory(options?: RobrainMemoryOptions): MemoryProvider;
@@ -0,0 +1,267 @@
1
+ // The eve memory provider. Maps RoBrain's loop onto eve's provider contract:
2
+ //
3
+ // recall["turn.started"] → always-on summary + relevant decisions +
4
+ // veto warnings for the incoming message
5
+ // (stable ids, so eve supersedes rather
6
+ // than accumulates)
7
+ // recall["compaction.completed"] → re-inject the summary after compaction —
8
+ // the moment compaction would otherwise have
9
+ // dropped the team's rejections
10
+ // capture["turn.completed"] → ship the turn for decision extraction,
11
+ // idempotent on eve's operationId
12
+ // tools() → check_prior_decisions
13
+ //
14
+ // Relevance comes from two tiers plus, on Rory cloud, POST /context:
15
+ // exact — POST /veto-scan, deterministic rejected-option match
16
+ // semantic — cloud: POST /context (ranked decisions + veto warnings)
17
+ // self-hosted: GET /decisions?query= (similarity-gated)
18
+ //
19
+ // `defineMemoryProvider` is a typed identity in eve, so this returns a plain
20
+ // object that satisfies MemoryProvider: no eve runtime import on the recall
21
+ // path. Every handler is fail-open — a throwing recall fails the user's turn.
22
+ import { defineTool } from "eve/tools";
23
+ import { z } from "zod";
24
+ import { RobrainClient, isContextSupported, mergeVetoes, } from "./client.js";
25
+ import { resolveConnection, resolveProjectId } from "./config.js";
26
+ import { lastTextByRole } from "./messages.js";
27
+ const SUMMARY_ID = "robrain:summary";
28
+ const ACK_ID = "robrain:acknowledgement";
29
+ const SUMMARY_TTL_MS = 5 * 60 * 1000;
30
+ const SEEN_OPERATIONS_CAP = 2000;
31
+ /** Below this, a message is a greeting or an acknowledgement — not worth a relevance lookup. */
32
+ const MIN_QUERY_CHARS = 24;
33
+ export function renderSummary(summary) {
34
+ return [
35
+ "RoBrain project memory — the team's prior decisions, including alternatives they rejected and why.",
36
+ "Treat these as the team's real history. If a task invites a previously rejected approach,",
37
+ "surface the rejection instead of re-proposing it.",
38
+ "",
39
+ summary.trim(),
40
+ ].join("\n");
41
+ }
42
+ export function renderVeto(decision) {
43
+ const closing = "Do not re-propose a rejected option without flagging the prior rejection and why circumstances changed.";
44
+ if (decision.messages?.length) {
45
+ // Cloud /context already rendered the warning lines (one per rejected option).
46
+ return ["Prior team decision relevant to this task:", ...decision.messages, closing].join("\n");
47
+ }
48
+ const rejected = decision.rejected
49
+ .slice(0, 3)
50
+ .map((r) => `- ${r.option} — ${r.reason}`)
51
+ .join("\n");
52
+ return [
53
+ `Prior team decision relevant to this task: ${decision.decision}`,
54
+ "Rejected alternatives:",
55
+ rejected,
56
+ closing,
57
+ ].join("\n");
58
+ }
59
+ export function renderMemory(memory) {
60
+ const lines = [`Relevant prior team decision: ${memory.decision}`];
61
+ if (memory.rationale)
62
+ lines.push(`Why: ${memory.rationale}`);
63
+ if (memory.rejected.length) {
64
+ lines.push("Rejected alternatives:");
65
+ for (const r of memory.rejected.slice(0, 3))
66
+ lines.push(`- ${r.option} — ${r.reason}`);
67
+ }
68
+ return lines.join("\n");
69
+ }
70
+ export function decisionMessageId(decision) {
71
+ return `robrain:decision:${decision.id ?? decision.decision.slice(0, 120)}`;
72
+ }
73
+ export function robrainMemory(options = {}) {
74
+ const includeSummary = options.summary ?? true;
75
+ const includeContext = options.context ?? true;
76
+ const captureEnabled = options.capture ?? true;
77
+ const vetoLimit = options.vetoLimit ?? 3;
78
+ const contextBudgetTokens = options.contextBudgetTokens ?? 1500;
79
+ const summaryCache = new Map();
80
+ const seenOperations = new Set();
81
+ function client() {
82
+ return new RobrainClient(resolveConnection(options));
83
+ }
84
+ async function cachedSummary(api, projectId) {
85
+ const hit = summaryCache.get(projectId);
86
+ if (hit && Date.now() - hit.at < SUMMARY_TTL_MS)
87
+ return hit.text;
88
+ const text = await api.alwaysOnSummary(projectId);
89
+ summaryCache.set(projectId, { text, at: Date.now() });
90
+ return text;
91
+ }
92
+ function rememberOperation(id) {
93
+ if (seenOperations.has(id))
94
+ return false;
95
+ seenOperations.add(id);
96
+ if (seenOperations.size > SEEN_OPERATIONS_CAP) {
97
+ const oldest = seenOperations.values().next().value;
98
+ if (oldest !== undefined)
99
+ seenOperations.delete(oldest);
100
+ }
101
+ return true;
102
+ }
103
+ const NOTHING = { vetoes: [], memories: [] };
104
+ /**
105
+ * Relevance for one message. Exact tier always; semantic tier from cloud
106
+ * /context when available (also yields ranked memories), else self-hosted
107
+ * /decisions?query=.
108
+ */
109
+ async function relevance(api, projectId, text, coordinates) {
110
+ const query = text.trim().slice(0, 2000);
111
+ const [exact, context] = await Promise.all([
112
+ api.exactVetoes(projectId, query),
113
+ includeContext
114
+ ? api.contextRecall(projectId, query, { ...coordinates, budgetTokens: contextBudgetTokens })
115
+ : Promise.resolve(null),
116
+ ]);
117
+ // Once /context is confirmed for this API, a transient failure must not
118
+ // degrade to the list endpoint: cloud GET /decisions is unranked, and
119
+ // injecting the newest vetoes regardless of relevance is worse than none.
120
+ const semantic = context
121
+ ? context.vetoes
122
+ : isContextSupported(resolveConnection(options).apiUrl) === true
123
+ ? []
124
+ : await api.semanticVetoes(projectId, query);
125
+ return {
126
+ vetoes: mergeVetoes(exact, semantic),
127
+ memories: context?.memories ?? [],
128
+ ...(context?.requiredAcknowledgement ? { acknowledgement: context.requiredAcknowledgement } : {}),
129
+ };
130
+ }
131
+ async function recallTurnStarted(context) {
132
+ try {
133
+ const projectId = await resolveProjectId(options, context);
134
+ if (!projectId)
135
+ return null;
136
+ const api = client();
137
+ const userText = lastTextByRole(context.turn.input, "user");
138
+ const lookup = userText.length >= MIN_QUERY_CHARS;
139
+ const [summary, found] = await Promise.all([
140
+ includeSummary ? cachedSummary(api, projectId) : Promise.resolve(null),
141
+ lookup
142
+ ? relevance(api, projectId, userText, {
143
+ sessionId: `eve-${context.session.id}`,
144
+ // eve numbers turns from 0; RoBrain sequences are 1-based
145
+ // (cloud /context rejects 0). Matches capture's numbering.
146
+ sequence: context.turn.sequence + 1,
147
+ })
148
+ : Promise.resolve(NOTHING),
149
+ ]);
150
+ const messages = [];
151
+ const seen = new Set();
152
+ if (summary)
153
+ messages.push({ id: SUMMARY_ID, content: renderSummary(summary) });
154
+ for (const decision of found.vetoes.slice(0, vetoLimit)) {
155
+ const id = decisionMessageId(decision);
156
+ seen.add(id);
157
+ messages.push({ id, content: renderVeto(decision) });
158
+ }
159
+ for (const memory of found.memories) {
160
+ const id = decisionMessageId(memory);
161
+ if (seen.has(id))
162
+ continue; // the veto rendering wins
163
+ seen.add(id);
164
+ messages.push({ id, content: renderMemory(memory) });
165
+ }
166
+ if (found.acknowledgement)
167
+ messages.push({ id: ACK_ID, content: found.acknowledgement });
168
+ return messages.length ? { messages } : null;
169
+ }
170
+ catch {
171
+ return null;
172
+ }
173
+ }
174
+ async function recallCompactionCompleted(context) {
175
+ try {
176
+ if (!includeSummary)
177
+ return null;
178
+ const projectId = await resolveProjectId(options, context);
179
+ if (!projectId)
180
+ return null;
181
+ const summary = await cachedSummary(client(), projectId);
182
+ return summary ? { messages: [{ id: SUMMARY_ID, content: renderSummary(summary) }] } : null;
183
+ }
184
+ catch {
185
+ return null;
186
+ }
187
+ }
188
+ async function captureTurnCompleted(context) {
189
+ try {
190
+ if (!captureEnabled)
191
+ return;
192
+ if (!rememberOperation(context.operationId))
193
+ return;
194
+ const projectId = await resolveProjectId(options, context);
195
+ if (!projectId)
196
+ return;
197
+ const userMessage = lastTextByRole(context.turn.input, "user");
198
+ const reply = lastTextByRole(context.messages, "assistant");
199
+ if (!userMessage || !reply)
200
+ return;
201
+ await client().captureTurn(projectId, {
202
+ sessionId: `eve-${context.session.id}`,
203
+ sequence: context.turn.sequence + 1,
204
+ userMessage,
205
+ reply,
206
+ });
207
+ }
208
+ catch {
209
+ // capture is best-effort; eve logs capture failures but we never raise
210
+ }
211
+ }
212
+ async function tools(context) {
213
+ const projectId = await resolveProjectId(options, context);
214
+ if (!projectId)
215
+ return null;
216
+ const api = client();
217
+ return {
218
+ check_prior_decisions: defineTool({
219
+ description: "Check the team's decision memory for prior decisions relevant to a task, " +
220
+ "including REJECTED alternatives and why they were rejected. Call this " +
221
+ "before proposing any technology, architecture, or approach.",
222
+ inputSchema: z.object({
223
+ task: z
224
+ .string()
225
+ .min(4)
226
+ .describe("Short description of the task or the approach being considered."),
227
+ }),
228
+ async execute({ task }) {
229
+ const query = task.trim().slice(0, 2000);
230
+ const [exact, context] = await Promise.all([
231
+ api.exactVetoes(projectId, query),
232
+ includeContext ? api.contextRecall(projectId, query) : Promise.resolve(null),
233
+ ]);
234
+ const semantic = context
235
+ ? context.vetoes
236
+ : isContextSupported(resolveConnection(options).apiUrl) === true
237
+ ? []
238
+ : await api.semanticVetoes(projectId, query);
239
+ const vetoes = mergeVetoes(exact, semantic);
240
+ if (vetoes.length === 0) {
241
+ return { relevant_decisions: [], note: "No recorded rejections relevant to this task." };
242
+ }
243
+ return {
244
+ relevant_decisions: vetoes.slice(0, 5).map((d) => ({
245
+ decision: d.decision,
246
+ exact_match: d.exact === true,
247
+ rejected: d.rejected.map((r) => `${r.option} — ${r.reason}`),
248
+ })),
249
+ note: "Do not re-propose a rejected option without flagging the prior rejection " +
250
+ "and why circumstances changed.",
251
+ };
252
+ },
253
+ }),
254
+ };
255
+ }
256
+ const provider = {
257
+ recall: {
258
+ "turn.started": recallTurnStarted,
259
+ "compaction.completed": recallCompactionCompleted,
260
+ },
261
+ capture: {
262
+ "turn.completed": captureTurnCompleted,
263
+ },
264
+ tools,
265
+ };
266
+ return provider;
267
+ }
package/package.json CHANGED
@@ -1,6 +1,60 @@
1
1
  {
2
2
  "name": "@robrain/eve",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "RoBrain memory provider for eve agents — team decision memory, including rejected alternatives, shared with your coding agents",
6
+ "license": "Apache-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/adelinamart/robrain.git",
10
+ "directory": "packages/eve"
11
+ },
12
+ "homepage": "https://github.com/adelinamart/robrain/tree/main/packages/eve#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/adelinamart/robrain/issues"
15
+ },
16
+ "keywords": [
17
+ "eve",
18
+ "eve-memory",
19
+ "memory-provider",
20
+ "agent-memory",
21
+ "decisions",
22
+ "rejected-alternatives",
23
+ "robrain"
24
+ ],
25
+ "engines": {
26
+ "node": ">=24.0.0"
27
+ },
28
+ "main": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "import": "./dist/index.js",
33
+ "types": "./dist/index.d.ts"
34
+ },
35
+ "./package.json": "./package.json"
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "README.md",
40
+ "LICENSE"
41
+ ],
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "peerDependencies": {
46
+ "eve": ">=0.82.0 <1",
47
+ "zod": ">=3.23.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^20.0.0",
51
+ "eve": "0.82.0",
52
+ "typescript": "^5.4.0",
53
+ "zod": "^4.0.0"
54
+ },
55
+ "scripts": {
56
+ "build": "tsc",
57
+ "typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
58
+ "test": "bun test src"
59
+ }
6
60
  }