ei-tui 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +170 -0
- package/package.json +63 -0
- package/src/README.md +96 -0
- package/src/cli/README.md +47 -0
- package/src/cli/commands/facts.ts +25 -0
- package/src/cli/commands/people.ts +25 -0
- package/src/cli/commands/quotes.ts +19 -0
- package/src/cli/commands/topics.ts +25 -0
- package/src/cli/commands/traits.ts +25 -0
- package/src/cli/retrieval.ts +269 -0
- package/src/cli.ts +176 -0
- package/src/core/AGENTS.md +104 -0
- package/src/core/embedding-service.ts +241 -0
- package/src/core/handlers/index.ts +1057 -0
- package/src/core/index.ts +4 -0
- package/src/core/llm-client.ts +265 -0
- package/src/core/model-context-windows.ts +49 -0
- package/src/core/orchestrators/ceremony.ts +500 -0
- package/src/core/orchestrators/extraction-chunker.ts +138 -0
- package/src/core/orchestrators/human-extraction.ts +457 -0
- package/src/core/orchestrators/index.ts +28 -0
- package/src/core/orchestrators/persona-generation.ts +76 -0
- package/src/core/orchestrators/persona-topics.ts +117 -0
- package/src/core/personas/index.ts +5 -0
- package/src/core/personas/opencode-agent.ts +81 -0
- package/src/core/processor.ts +1413 -0
- package/src/core/queue-processor.ts +197 -0
- package/src/core/state/checkpoints.ts +68 -0
- package/src/core/state/human.ts +176 -0
- package/src/core/state/index.ts +5 -0
- package/src/core/state/personas.ts +217 -0
- package/src/core/state/queue.ts +144 -0
- package/src/core/state-manager.ts +347 -0
- package/src/core/types.ts +421 -0
- package/src/core/utils/decay.ts +33 -0
- package/src/index.ts +1 -0
- package/src/integrations/opencode/importer.ts +896 -0
- package/src/integrations/opencode/index.ts +16 -0
- package/src/integrations/opencode/json-reader.ts +304 -0
- package/src/integrations/opencode/reader-factory.ts +35 -0
- package/src/integrations/opencode/sqlite-reader.ts +189 -0
- package/src/integrations/opencode/types.ts +244 -0
- package/src/prompts/AGENTS.md +62 -0
- package/src/prompts/ceremony/description-check.ts +47 -0
- package/src/prompts/ceremony/expire.ts +30 -0
- package/src/prompts/ceremony/explore.ts +60 -0
- package/src/prompts/ceremony/index.ts +11 -0
- package/src/prompts/ceremony/types.ts +42 -0
- package/src/prompts/generation/descriptions.ts +91 -0
- package/src/prompts/generation/index.ts +15 -0
- package/src/prompts/generation/persona.ts +155 -0
- package/src/prompts/generation/seeds.ts +31 -0
- package/src/prompts/generation/types.ts +47 -0
- package/src/prompts/heartbeat/check.ts +179 -0
- package/src/prompts/heartbeat/ei.ts +208 -0
- package/src/prompts/heartbeat/index.ts +15 -0
- package/src/prompts/heartbeat/types.ts +70 -0
- package/src/prompts/human/fact-scan.ts +152 -0
- package/src/prompts/human/index.ts +32 -0
- package/src/prompts/human/item-match.ts +74 -0
- package/src/prompts/human/item-update.ts +322 -0
- package/src/prompts/human/person-scan.ts +115 -0
- package/src/prompts/human/topic-scan.ts +135 -0
- package/src/prompts/human/trait-scan.ts +115 -0
- package/src/prompts/human/types.ts +127 -0
- package/src/prompts/index.ts +90 -0
- package/src/prompts/message-utils.ts +39 -0
- package/src/prompts/persona/index.ts +16 -0
- package/src/prompts/persona/topics-match.ts +69 -0
- package/src/prompts/persona/topics-scan.ts +98 -0
- package/src/prompts/persona/topics-update.ts +157 -0
- package/src/prompts/persona/traits.ts +117 -0
- package/src/prompts/persona/types.ts +74 -0
- package/src/prompts/response/index.ts +147 -0
- package/src/prompts/response/sections.ts +355 -0
- package/src/prompts/response/types.ts +38 -0
- package/src/prompts/validation/ei.ts +93 -0
- package/src/prompts/validation/index.ts +6 -0
- package/src/prompts/validation/types.ts +22 -0
- package/src/storage/crypto.ts +96 -0
- package/src/storage/index.ts +5 -0
- package/src/storage/interface.ts +9 -0
- package/src/storage/local.ts +79 -0
- package/src/storage/merge.ts +69 -0
- package/src/storage/remote.ts +145 -0
- package/src/templates/welcome.ts +91 -0
- package/tui/README.md +62 -0
- package/tui/bunfig.toml +4 -0
- package/tui/src/app.tsx +55 -0
- package/tui/src/commands/archive.tsx +93 -0
- package/tui/src/commands/context.tsx +124 -0
- package/tui/src/commands/delete.tsx +71 -0
- package/tui/src/commands/details.tsx +41 -0
- package/tui/src/commands/editor.tsx +46 -0
- package/tui/src/commands/help.tsx +12 -0
- package/tui/src/commands/me.tsx +145 -0
- package/tui/src/commands/model.ts +47 -0
- package/tui/src/commands/new.ts +31 -0
- package/tui/src/commands/pause.ts +46 -0
- package/tui/src/commands/persona.tsx +58 -0
- package/tui/src/commands/provider.tsx +124 -0
- package/tui/src/commands/quit.ts +22 -0
- package/tui/src/commands/quotes.tsx +172 -0
- package/tui/src/commands/registry.test.ts +137 -0
- package/tui/src/commands/registry.ts +130 -0
- package/tui/src/commands/resume.ts +39 -0
- package/tui/src/commands/setsync.tsx +43 -0
- package/tui/src/commands/settings.tsx +83 -0
- package/tui/src/components/ConfirmOverlay.tsx +51 -0
- package/tui/src/components/ConflictOverlay.tsx +78 -0
- package/tui/src/components/HelpOverlay.tsx +69 -0
- package/tui/src/components/Layout.tsx +24 -0
- package/tui/src/components/MessageList.tsx +174 -0
- package/tui/src/components/PersonaListOverlay.tsx +186 -0
- package/tui/src/components/PromptInput.tsx +145 -0
- package/tui/src/components/ProviderListOverlay.tsx +208 -0
- package/tui/src/components/QuotesOverlay.tsx +157 -0
- package/tui/src/components/Sidebar.tsx +95 -0
- package/tui/src/components/StatusBar.tsx +77 -0
- package/tui/src/components/WelcomeOverlay.tsx +73 -0
- package/tui/src/context/ei.tsx +623 -0
- package/tui/src/context/keyboard.tsx +164 -0
- package/tui/src/context/overlay.tsx +53 -0
- package/tui/src/index.tsx +8 -0
- package/tui/src/storage/file.ts +185 -0
- package/tui/src/util/duration.ts +32 -0
- package/tui/src/util/editor.ts +188 -0
- package/tui/src/util/logger.ts +109 -0
- package/tui/src/util/persona-editor.tsx +181 -0
- package/tui/src/util/provider-editor.tsx +168 -0
- package/tui/src/util/syntax.ts +35 -0
- package/tui/src/util/yaml-serializers.ts +755 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jeremy Scherer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Ei
|
|
2
|
+
|
|
3
|
+
A local-first AI companion system with persistent personas and Opencode Integration.
|
|
4
|
+
|
|
5
|
+
You can access the Web version at [ei.flare576.com](https://ei.flare576.com).
|
|
6
|
+
|
|
7
|
+
You can install the local version via `npm install -g ei-tui` (see [### TUI](#tui) for details).
|
|
8
|
+
|
|
9
|
+
If you're here to give Opencode perpetual memory (yes), jump over to [TUI README.md](./tui/README.md) to learn how to get information _into_ Ei, and [CLI README.md](./src/cli/README.md) to get it back _out_.
|
|
10
|
+
|
|
11
|
+
## What Does "Local First" Mean?
|
|
12
|
+
|
|
13
|
+
All of the data Ei learns about you from your conversations is stored on your device (LocalStorage on the Web, and `$EI_DATA_PATH` or `~/.local/share/ei` in the TUI).
|
|
14
|
+
|
|
15
|
+
Unless you enable Syncing, that's where it stays.
|
|
16
|
+
|
|
17
|
+
If you have a local LLM, literally no data leaves your system(s) by default. If you don't, you'll need to provide an LLM for Ei to use. I tried to make that as easy as possible via adding Providers via API Key.
|
|
18
|
+
|
|
19
|
+
There's no other usage, debugging, analytics, tracking, or history information stored or transmitted - anonymized or otherwise.
|
|
20
|
+
|
|
21
|
+
If there's a problem with the system, you need to tell me here on GitHub, or on Bluesky, or Discord, or whatever. There's no "report a bug" button, no "DONATE" link in the app.
|
|
22
|
+
|
|
23
|
+
Don't get me wrong -I absolutely want to fix whatever problem you run into, or hear about the feature you want - but your Ei system, and the data you build with it, is yours.
|
|
24
|
+
|
|
25
|
+
That's what "Local First" means.
|
|
26
|
+
|
|
27
|
+
### What Does Sync Do?
|
|
28
|
+
|
|
29
|
+
Optionally, you can choose to "Sync" to flare576.com. The only reason you would do this is if you wanted to easily move between two or more devices.
|
|
30
|
+
|
|
31
|
+
If you just want data back-ups, there's an "Backup & Restore" feature built into the system on the same page as "Sync" (actually, above Sync, because I honestly don't think anyone besides me wants to use Ei enough to use two devices...).
|
|
32
|
+
|
|
33
|
+
After you enable it, Sync kicks in when you close the TUI, or if you click "Save and Exit" in the web app. It sends a single, encrypted file to a file store for Ei...
|
|
34
|
+
|
|
35
|
+
That I can't decrypt.
|
|
36
|
+
|
|
37
|
+
Even if I wanted to (I definitely do not), I wouldn't be able to divulge your information because **You** are the only one that can generate the key. It's not a public/private keypair, it's not a "handshake".
|
|
38
|
+
|
|
39
|
+
It's your data - I have no right to it, and neither does anyone else except you.
|
|
40
|
+
|
|
41
|
+
## What's a Persona?
|
|
42
|
+
|
|
43
|
+
At the core of the technology, LLM "Agents" are made up of two or three components, depending on who you ask:
|
|
44
|
+
|
|
45
|
+
1. System Prompt
|
|
46
|
+
2. User Prompt
|
|
47
|
+
a. Which can be broken into "Messages", but they're still basically the User Prompt
|
|
48
|
+
|
|
49
|
+
The "System Prompt" is the part where you usually say
|
|
50
|
+
|
|
51
|
+
> You are a pirate
|
|
52
|
+
|
|
53
|
+
The "User Prompt" is the part where you put your messages
|
|
54
|
+
|
|
55
|
+
> user: "OMG ARE YOU REALLY A PIRATE?!"
|
|
56
|
+
> assistant: "Yar."
|
|
57
|
+
|
|
58
|
+
A "Persona" is the combination of these two pieces of data, plus some _personality_. The reason I didn't call it an "Agent" is because Personas aren't static<sup>1</sup> - they'll grow and adapt as you talk to them. See the [Core Readme](core/README.md) for more information!
|
|
59
|
+
|
|
60
|
+
> <sup>1</sup>: By default. You can make them static.
|
|
61
|
+
|
|
62
|
+
## The Basics
|
|
63
|
+
|
|
64
|
+
Ei can operate with three types of input, and three types of output.
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
[TUI] -User Messages-> Ei <-User Messages- [Web]
|
|
68
|
+
^
|
|
69
|
+
Sessions
|
|
70
|
+
|
|
|
71
|
+
[OpenCode]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
[TUI] <-Persona Messages- Ei -Persona Messages-> [Web]
|
|
76
|
+
|
|
|
77
|
+
CLI Data
|
|
78
|
+
v
|
|
79
|
+
[OpenCode]
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Optionally, users can opt into a server-side data sync. This is ideal for users who want to use multiple devices or switch between TUI and Web throughout the day. All data is encrypted _before_ being sent to the server, using a key that only the user can generate (your `username` and `passphrase` never leave your device - I couldn't decrypt your data if I wanted to).
|
|
83
|
+
|
|
84
|
+
### Web
|
|
85
|
+
|
|
86
|
+
When you access Ei via https://ei.flare576.com, your browser will download the assets and walk you through onboarding. If you're running a Local LLM on port :1234 it will auto-detect it, otherwise it will allowing you to enter it.
|
|
87
|
+
|
|
88
|
+
Then you'll land on the chat interface. As you enter messages, they'll go to *YOUR* server. As Ei discovers information about you, summaries will be built with *YOUR* server, and data will be stored to *YOUR* LocalStorage in *YOUR* browser.
|
|
89
|
+
|
|
90
|
+
When you leave, it simply stays in LocalStorage. When you come back, it loads it from LocalStorage.
|
|
91
|
+
|
|
92
|
+
More information can be found in the [Web Readme](web/README.md)
|
|
93
|
+
|
|
94
|
+
### TUI
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
npm install -g ei-tui
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
When you install Ei, you pull down this package and it's dependencies.
|
|
101
|
+
|
|
102
|
+
If you have a Local LLM, that's the first and last set of signals that leave your machine for Ei unless you tell it otherwise.
|
|
103
|
+
|
|
104
|
+
Regardless, Running `ei` pops open the TUI interface and, just like on the web, all messages and summary requests flow to your LLM provider, but the core data stays on your device.
|
|
105
|
+
|
|
106
|
+
More information (including commands) can be found in the [TUI Readme](tui/README.md)
|
|
107
|
+
|
|
108
|
+
### Opencode
|
|
109
|
+
|
|
110
|
+
Opencode saves all of its sessions locally, either in a JSON structure or, if you're running the latest version, in a SQLite DB. If you enable the integration, Ei will pull all of the conversational parts of those sessions and summarize them, pulling out details, quotes, and keeping the summaries up-to-date.
|
|
111
|
+
|
|
112
|
+
Then, Opencode can call into Ei and pull those details back out.
|
|
113
|
+
|
|
114
|
+
Yes, I did make a dynamic, perpetual RAG. No, I didn't do it on purpose; that's why you always have a side-project or two going. See [TUI Readme](tui/README.md)
|
|
115
|
+
|
|
116
|
+
## Technical Details
|
|
117
|
+
|
|
118
|
+
This project is separated into five (5) logical parts:
|
|
119
|
+
|
|
120
|
+
1. Ei Core
|
|
121
|
+
a. Location: `/src`
|
|
122
|
+
b. Purpose: Shared between TUI and Web, it's The event-driven core of Ei, housing:
|
|
123
|
+
i. Business logic
|
|
124
|
+
ii. Prompts
|
|
125
|
+
iii. Integrations
|
|
126
|
+
2. Ei Online
|
|
127
|
+
a. Location: `/web`
|
|
128
|
+
b. Purpose: Provides a web interface for Ei.
|
|
129
|
+
c. Deployed to: https://ei.flare576.com
|
|
130
|
+
3. Ei Terminal User Interface (TUI)
|
|
131
|
+
a. Location: `/tui`
|
|
132
|
+
b. Purpose: Provides a TUI interface for Ei
|
|
133
|
+
c. Deployed to: NPM for you to install
|
|
134
|
+
4. Ei API
|
|
135
|
+
a. Location: `/api`
|
|
136
|
+
b. Purpose: Provides remote sync for Ei.
|
|
137
|
+
c. Deployed to: https://ei.flare576.com/api
|
|
138
|
+
5. Ei Command Line Interface (CLI)
|
|
139
|
+
a. Location: `/src/cli`
|
|
140
|
+
b. Purpose: Provides a CLI interface for Opencode to use as a tool
|
|
141
|
+
c. Technically, ships with the TUI
|
|
142
|
+
|
|
143
|
+
## Requirements
|
|
144
|
+
[Bun](https://bun.sh) runtime (>=1.0.0)
|
|
145
|
+
Local LLM provider (LM Studio, Ollama, etc.)
|
|
146
|
+
* OR API access to a remote LLM host (Anthropic, OpenAI, Bedrock, your uncle's LLM farm, etc.)
|
|
147
|
+
|
|
148
|
+
## LM Studio Setup
|
|
149
|
+
|
|
150
|
+
**Important**: You must enable CORS in LM Studio for browser-based EI to work.
|
|
151
|
+
|
|
152
|
+
1. Open LM Studio
|
|
153
|
+
2. Go to **Local Server** tab (left sidebar)
|
|
154
|
+
3. Enable **"Enable CORS"** toggle
|
|
155
|
+
4. Start/restart the server
|
|
156
|
+
|
|
157
|
+
Without this setting, browser security policies will block API calls.
|
|
158
|
+
|
|
159
|
+
## Development
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
npm install
|
|
163
|
+
npm run dev # Watch mode
|
|
164
|
+
npm run build # Compile TypeScript
|
|
165
|
+
npm run test # Run tests
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Project Structure
|
|
169
|
+
|
|
170
|
+
See `AGENTS.md` for detailed architecture and contribution guidelines.
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ei-tui",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"author": "Flare576",
|
|
5
|
+
"engines": {
|
|
6
|
+
"bun": ">=1.0.0"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"src/",
|
|
10
|
+
"tui/src/",
|
|
11
|
+
"tui/bunfig.toml",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@playwright/test": "^1.58.0",
|
|
17
|
+
"@types/bun": "latest",
|
|
18
|
+
"@types/express": "^5.0.6",
|
|
19
|
+
"@types/node": "^25.0.10",
|
|
20
|
+
"@vitest/coverage-v8": "^2.1.9",
|
|
21
|
+
"express": "^5.2.1",
|
|
22
|
+
"typescript": "^5.3.0",
|
|
23
|
+
"vitest": "^2.0.0"
|
|
24
|
+
},
|
|
25
|
+
"bin": {
|
|
26
|
+
"ei": "./src/cli.ts"
|
|
27
|
+
},
|
|
28
|
+
"description": "Ei - Local-first AI companion with persistent personas",
|
|
29
|
+
"keywords": [
|
|
30
|
+
"ai",
|
|
31
|
+
"llm",
|
|
32
|
+
"personas",
|
|
33
|
+
"tui",
|
|
34
|
+
"cli",
|
|
35
|
+
"companion"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"scripts": {
|
|
39
|
+
"dev": "tsc --watch",
|
|
40
|
+
"build": "tsc",
|
|
41
|
+
"test": "vitest run && npm run test:tui",
|
|
42
|
+
"test:src": "vitest run",
|
|
43
|
+
"test:tui": "cd tui && npm run test",
|
|
44
|
+
"test:watch": "vitest",
|
|
45
|
+
"test:coverage": "vitest run --coverage",
|
|
46
|
+
"test:e2e": "playwright test",
|
|
47
|
+
"test:e2e:tui": "cd tui && npm run test:e2e",
|
|
48
|
+
"test:e2e:ui": "playwright test --ui",
|
|
49
|
+
"test:e2e:debug": "playwright test --debug",
|
|
50
|
+
"test:all": "npm run test && npm run test:e2e && npm run test:e2e:tui",
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"web": "cd web && npm run dev",
|
|
53
|
+
"tui": "cd tui && npm run dev"
|
|
54
|
+
},
|
|
55
|
+
"type": "module",
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@opentui/core": "^0.1.79",
|
|
58
|
+
"@opentui/solid": "^0.1.79",
|
|
59
|
+
"fastembed": "^2.1.0",
|
|
60
|
+
"solid-js": "1.9.9",
|
|
61
|
+
"yaml": "^2.8.2"
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Behind The Curtain
|
|
2
|
+
|
|
3
|
+
Welcome to the Core of Ei. If you're reading this, you're probably interested about the inner-workings of the system.
|
|
4
|
+
|
|
5
|
+
Or you're lost. That's ok, too.
|
|
6
|
+
|
|
7
|
+
# Data Models
|
|
8
|
+
|
|
9
|
+
There are two distinct types of data: Human and Persona.
|
|
10
|
+
|
|
11
|
+
## Human
|
|
12
|
+
|
|
13
|
+
Human data is sort of the "Global" data - Each Persona can read and write elements to the humans Facts, Traits, People, and Topics. In addition, there are "Quotes" that can tie to those four types of data.
|
|
14
|
+
|
|
15
|
+
As the user uses the system, it tries to keep track of several data points for these elements:
|
|
16
|
+
|
|
17
|
+
- Sentiment: How much the system thinks the user likes or dislikes the idea or concept
|
|
18
|
+
* 1.0 means that there is nothing in the world that the user loves more than this thing
|
|
19
|
+
* -1.0 means that, as far as the user is concerned, the universe never needed this thing
|
|
20
|
+
- Exposure: Topics and People have an "Exposure" measure, gauging how frequently they're discussed
|
|
21
|
+
* Desired: How much the user *wants* to talk about a subject, where:
|
|
22
|
+
+ 0.0: The user never EVER wants to talk about or hear about the subject
|
|
23
|
+
+ 1.0: Every message to and from the user should be about this person, place, or thing
|
|
24
|
+
* Current: How much the user has talked or heard about a subject, where:
|
|
25
|
+
+ 0.0: Obi-Wan Kenobi ...now that’s a name I’ve not heard in a long time
|
|
26
|
+
+ 1.0: The user just spent 4 hours talking about Star Wars
|
|
27
|
+
|
|
28
|
+
Each of those types represents a piece of what the system "knows" about the person, and all but "Traits" are kept up-to-date as the person chats with Personas, but not on always on every message. On each message to a Persona, a check is made:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
if(Person.newMessages > count_of_human_[type]) {
|
|
32
|
+
run extract[Type]
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Again, except for Traits<sup>1</sup>, this is to extract quotes, description updates, title updates, etc. for the conversations the user is having, and keep them feeling alive.
|
|
37
|
+
|
|
38
|
+
> <sup>1</sup> Traits are unique because, after trying to extract them in the same way as the other pieces of data, I realized that it's sorta hard to understand a core aspect of someone in one message, or even 10. Even doing this analysis over a full 24 hours hasn't proven to be particularly effective, but it's the best we have so far.
|
|
39
|
+
|
|
40
|
+
## Persona
|
|
41
|
+
|
|
42
|
+
The Persona data is designed with two goals: Consistency (Traits) and Growth (Topics)
|
|
43
|
+
|
|
44
|
+
### Consistency (Traits)
|
|
45
|
+
|
|
46
|
+
"One-Eyed-Willy talks like a pirate" is an example of a Persona Trait - you can assign it a Sentiment ("He LOVES talking like a pirate") as well as a Strength ("He does it ALL THE TIME"). These are things that you can set during creation, or that you can add over time with direct requests "Please talk like a pirate less often" or directly in the Persona editor. These don't "gradually" change over time.
|
|
47
|
+
|
|
48
|
+
### Growth (Topics)
|
|
49
|
+
|
|
50
|
+
"Pirate Treasure Procurement" is an example of a Persona Topic. These grow and adapt over time, and have four characteristics:
|
|
51
|
+
|
|
52
|
+
- Perspective: "It's a pirates duty to plunder booty"
|
|
53
|
+
- Approach: "Full sail ahead, take what you can, give nothing back"
|
|
54
|
+
- Personal Stake: "A ship sails not on hunger and wont."
|
|
55
|
+
- Sentiment: 0.9 // The only thing One-Eyed-Willy likes more than Pirate Treasure Procurement is ~rum~ Baby Ruth
|
|
56
|
+
|
|
57
|
+
Each Topic will have an "exposure" rating similar to those on Human Data points.
|
|
58
|
+
|
|
59
|
+
# Ceremony Intent
|
|
60
|
+
|
|
61
|
+
Every 24 hours, we want to freshen up the system. We do this in 4 parts: Exposure, Decay, Expire, Explore
|
|
62
|
+
|
|
63
|
+
## Exposure
|
|
64
|
+
|
|
65
|
+
I also frequently refer to this as "Extract," but this is the first step where we determine what the human and that Persona talked about that day. It serves two purposes:
|
|
66
|
+
|
|
67
|
+
### Detail Extraction
|
|
68
|
+
|
|
69
|
+
Since we also pull out details during normal discourse (see above), this is the less-important step at this point, but still vital for catching up with the last few messages, or Personas that only received a few messages during the day and may not have hit the current limit for natural extraction.
|
|
70
|
+
|
|
71
|
+
Additionally, this is the ONLY time when Human Traits are created or updated - after (hopefully) enough messages have been exchanged for an agent to analyze it and say "Yup, Flare is _definitely_ verbose."
|
|
72
|
+
|
|
73
|
+
### Exposure Adjustment
|
|
74
|
+
|
|
75
|
+
Exposure is calculated by two metrics - `desired` and `current`. If an entity REALLY likes talking about a subject, their `desired` will be very high (1.0 max), ranging down to 0.0 for subjects which that entity does NOT wish to discuss. You may have guessed already, but `current` is how much they've recently talked about a topic.
|
|
76
|
+
|
|
77
|
+
Adjusting the values is different for Human Topics/People than Persona Topics. The Human subjects are actually adjusted during the previous step, while extracting details.
|
|
78
|
+
|
|
79
|
+
The Persona Topic update only happens during the Ceremony, and really this step only increases exposure IF the subject was discussed, bumping the last_updated field accordingly.
|
|
80
|
+
|
|
81
|
+
## Decay
|
|
82
|
+
|
|
83
|
+
After we determine if topics were discussed (increasing exposure), we adjust exposure the _other_ way. Based on some heuristics (like current level, desired level, and time-since-discussion), we decrease the current exposure levels down.
|
|
84
|
+
|
|
85
|
+
## Expire
|
|
86
|
+
|
|
87
|
+
This and the following step (Explore) are exclusive to Persona Topics right now. In Expire, we analyze the Person Topics to determine if any of them have
|
|
88
|
+
- Lost their meaning to the Persona
|
|
89
|
+
- Been ignored or dismissed by the user
|
|
90
|
+
|
|
91
|
+
This is largely tracked by exposure, but expiration is dictated by an Agent.
|
|
92
|
+
|
|
93
|
+
## Explore
|
|
94
|
+
|
|
95
|
+
After we've removed irrelevant topics, this is the Agent's opportunity to add NEW topics that might be of interest to the Persona (and the user). Again, it's a prompt to an agent if the Persona doesn't have its full capacity of Topics.
|
|
96
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# The CLI
|
|
2
|
+
|
|
3
|
+
It's actually super straight-forward
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
ei # Start the TUI
|
|
7
|
+
ei "query string" # Return up to 10 "query string" across all types
|
|
8
|
+
ei -n 5 "query string" # Return up to 5 results
|
|
9
|
+
ei facts -n 5 "query string" # Return up to 5 facts
|
|
10
|
+
ei traits -n 5 "query string" # Return up to 5 traits
|
|
11
|
+
ei people -n 5 "query string" # Return up to 5 people
|
|
12
|
+
ei topics -n 5 "query string" # Return up to 5 topics
|
|
13
|
+
ei quotes -n 5 "query string" # Return up to 5 quotes
|
|
14
|
+
ei --id <id> # Look up a specific entity by ID
|
|
15
|
+
echo <id> | ei --id # Look up entity by ID from stdin
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
# An Agentic Tool
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
The `--id` flag is designed for piping. For example, search for a topic and then fetch the full entity:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
ei "memory leak" | jq '.[0].id' | ei --id
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
To register Ei as an explicit OpenCode tool (optional — agents can also just call `ei` via shell):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
mkdir -p ~/.config/opencode/tools
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Create `~/.config/opencode/tools/ei.ts`:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { tool } from "@opencode-ai/plugin"
|
|
37
|
+
|
|
38
|
+
export default tool({
|
|
39
|
+
description: "Search the user's Ei knowledge base for facts, people, topics, traits, and quotes",
|
|
40
|
+
args: {
|
|
41
|
+
query: tool.schema.string().describe("Search query"),
|
|
42
|
+
},
|
|
43
|
+
async execute(args) {
|
|
44
|
+
return await Bun.$`ei ${args.query}`.text()
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { loadLatestState, retrieve } from "../retrieval";
|
|
2
|
+
import type { FactResult } from "../retrieval";
|
|
3
|
+
|
|
4
|
+
export async function execute(query: string, limit: number): Promise<FactResult[]> {
|
|
5
|
+
const state = await loadLatestState();
|
|
6
|
+
if (!state) {
|
|
7
|
+
console.error("No saved state found. Is EI_DATA_PATH set correctly?");
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const facts = state.human.facts;
|
|
12
|
+
if (facts.length === 0) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const results = await retrieve(facts, query, limit);
|
|
17
|
+
|
|
18
|
+
return results.map(fact => ({
|
|
19
|
+
id: fact.id,
|
|
20
|
+
name: fact.name,
|
|
21
|
+
description: fact.description,
|
|
22
|
+
sentiment: fact.sentiment,
|
|
23
|
+
validated: fact.validated,
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { loadLatestState, retrieve } from "../retrieval";
|
|
2
|
+
import type { PersonResult } from "../retrieval";
|
|
3
|
+
|
|
4
|
+
export async function execute(query: string, limit: number): Promise<PersonResult[]> {
|
|
5
|
+
const state = await loadLatestState();
|
|
6
|
+
if (!state) {
|
|
7
|
+
console.error("No saved state found. Is EI_DATA_PATH set correctly?");
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const people = state.human.people;
|
|
12
|
+
if (people.length === 0) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const results = await retrieve(people, query, limit);
|
|
17
|
+
|
|
18
|
+
return results.map(person => ({
|
|
19
|
+
id: person.id,
|
|
20
|
+
name: person.name,
|
|
21
|
+
description: person.description,
|
|
22
|
+
relationship: person.relationship,
|
|
23
|
+
sentiment: person.sentiment,
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { loadLatestState, retrieve, mapQuote } from "../retrieval";
|
|
2
|
+
import type { QuoteResult } from "../retrieval";
|
|
3
|
+
|
|
4
|
+
export async function execute(query: string, limit: number): Promise<QuoteResult[]> {
|
|
5
|
+
const state = await loadLatestState();
|
|
6
|
+
if (!state) {
|
|
7
|
+
console.error("No saved state found. Is EI_DATA_PATH set correctly?");
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const quotes = state.human.quotes;
|
|
12
|
+
if (quotes.length === 0) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const results = await retrieve(quotes, query, limit);
|
|
17
|
+
|
|
18
|
+
return results.map(quote => mapQuote(quote, state));
|
|
19
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { loadLatestState, retrieve } from "../retrieval";
|
|
2
|
+
import type { TopicResult } from "../retrieval";
|
|
3
|
+
|
|
4
|
+
export async function execute(query: string, limit: number): Promise<TopicResult[]> {
|
|
5
|
+
const state = await loadLatestState();
|
|
6
|
+
if (!state) {
|
|
7
|
+
console.error("No saved state found. Is EI_DATA_PATH set correctly?");
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const topics = state.human.topics;
|
|
12
|
+
if (topics.length === 0) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const results = await retrieve(topics, query, limit);
|
|
17
|
+
|
|
18
|
+
return results.map(topic => ({
|
|
19
|
+
id: topic.id,
|
|
20
|
+
name: topic.name,
|
|
21
|
+
description: topic.description,
|
|
22
|
+
category: topic.category,
|
|
23
|
+
sentiment: topic.sentiment,
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { loadLatestState, retrieve } from "../retrieval";
|
|
2
|
+
import type { TraitResult } from "../retrieval";
|
|
3
|
+
|
|
4
|
+
export async function execute(query: string, limit: number): Promise<TraitResult[]> {
|
|
5
|
+
const state = await loadLatestState();
|
|
6
|
+
if (!state) {
|
|
7
|
+
console.error("No saved state found. Is EI_DATA_PATH set correctly?");
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const traits = state.human.traits;
|
|
12
|
+
if (traits.length === 0) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const results = await retrieve(traits, query, limit);
|
|
17
|
+
|
|
18
|
+
return results.map(trait => ({
|
|
19
|
+
id: trait.id,
|
|
20
|
+
name: trait.name,
|
|
21
|
+
description: trait.description,
|
|
22
|
+
strength: trait.strength ?? 0.5,
|
|
23
|
+
sentiment: trait.sentiment,
|
|
24
|
+
}));
|
|
25
|
+
}
|