@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 +202 -0
- package/README.md +103 -2
- package/dist/client.d.ts +75 -0
- package/dist/client.js +192 -0
- package/dist/config.d.ts +53 -0
- package/dist/config.js +52 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +11 -0
- package/dist/messages.d.ts +8 -0
- package/dist/messages.js +32 -0
- package/dist/provider.d.ts +11 -0
- package/dist/provider.js +267 -0
- package/package.json +57 -3
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
|
-
#
|
|
1
|
+
# @robrain/eve
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
package/dist/client.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
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;
|
package/dist/messages.js
ADDED
|
@@ -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;
|
package/dist/provider.js
ADDED
|
@@ -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.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
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
|
}
|