@mr-jones123/toji 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +158 -0
- package/package.json +47 -0
- package/packages/toji-comms/README.md +71 -0
- package/packages/toji-comms/src/cli/agents.ts +121 -0
- package/packages/toji-comms/src/cli/mmx.ts +65 -0
- package/packages/toji-comms/src/cli/subprocess.ts +47 -0
- package/packages/toji-comms/src/comms/orchestrator.ts +92 -0
- package/packages/toji-comms/src/comms/prompt.ts +84 -0
- package/packages/toji-comms/src/comms/store.ts +145 -0
- package/packages/toji-comms/src/comms/types.ts +94 -0
- package/packages/toji-comms/src/db/connection.ts +58 -0
- package/packages/toji-comms/src/db/migrations.ts +69 -0
- package/packages/toji-comms/src/index.ts +368 -0
- package/packages/toji-comms/src/mcp/client.ts +71 -0
- package/packages/toji-comms/src/mcp/server.ts +81 -0
- package/packages/toji-mem/README.md +52 -0
- package/packages/toji-mem/grammars/manifest.json +9 -0
- package/packages/toji-mem/grammars/tree-sitter-cpp.wasm +0 -0
- package/packages/toji-mem/grammars/tree-sitter-dart.wasm +0 -0
- package/packages/toji-mem/grammars/tree-sitter-java.wasm +0 -0
- package/packages/toji-mem/grammars/tree-sitter-javascript.wasm +0 -0
- package/packages/toji-mem/grammars/tree-sitter-python.wasm +0 -0
- package/packages/toji-mem/grammars/tree-sitter-tsx.wasm +0 -0
- package/packages/toji-mem/grammars/tree-sitter-typescript.wasm +0 -0
- package/packages/toji-mem/src/db/connection.ts +58 -0
- package/packages/toji-mem/src/db/migrations.ts +181 -0
- package/packages/toji-mem/src/index.ts +326 -0
- package/packages/toji-mem/src/indexer/file-walker.ts +45 -0
- package/packages/toji-mem/src/indexer/index-project.ts +277 -0
- package/packages/toji-mem/src/indexer/parsers/cpp.ts +81 -0
- package/packages/toji-mem/src/indexer/parsers/dart.ts +91 -0
- package/packages/toji-mem/src/indexer/parsers/java.ts +83 -0
- package/packages/toji-mem/src/indexer/parsers/python.ts +84 -0
- package/packages/toji-mem/src/indexer/parsers/registry.ts +28 -0
- package/packages/toji-mem/src/indexer/parsers/tree-sitter-loader.ts +39 -0
- package/packages/toji-mem/src/indexer/parsers/types.ts +48 -0
- package/packages/toji-mem/src/indexer/parsers/typescript.ts +105 -0
- package/packages/toji-mem/src/standards/store.ts +52 -0
- package/packages/toji-mem/src/tools/blast-radius.ts +98 -0
- package/packages/toji-mem/src/tools/graph-explore.ts +186 -0
- package/packages/toji-mem/src/tools/project-overview.ts +102 -0
- package/packages/toji-mem/src/tools/query-memory.ts +105 -0
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Toji
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
Toji is a Pi extension workspace with two packages:
|
|
9
|
+
|
|
10
|
+
- `toji-mem`: local code memory, search, graph traversal, and benchmarks
|
|
11
|
+
- `toji-comms`: AI-to-AI communication bridge for Pi agents
|
|
12
|
+
|
|
13
|
+
## What `toji-mem` does
|
|
14
|
+
|
|
15
|
+
`toji-mem` indexes a repository into SQLite and answers common agent questions:
|
|
16
|
+
|
|
17
|
+
- where is this symbol?
|
|
18
|
+
- what file should I read?
|
|
19
|
+
- what related files/symbols are nearby?
|
|
20
|
+
- what might be affected if I change this symbol?
|
|
21
|
+
|
|
22
|
+
It uses:
|
|
23
|
+
|
|
24
|
+
- SQLite FTS5 for fuzzy search over files, symbols, docstrings, and paths
|
|
25
|
+
- B-tree indexes for exact file and symbol lookups
|
|
26
|
+
- Tree-sitter parsers for symbols, imports, and calls
|
|
27
|
+
- graph edges for blast-radius and related-file traversal
|
|
28
|
+
|
|
29
|
+
## What `toji-comms` does
|
|
30
|
+
|
|
31
|
+
`toji-comms` lets the active Pi model send a structured message to a peer model through MCP/mmx, then stores the discussion in SQLite.
|
|
32
|
+
|
|
33
|
+
It provides:
|
|
34
|
+
|
|
35
|
+
- `toji_comms_send` for AI-authored peer messages
|
|
36
|
+
- `toji_plan_run` as a compatibility alias for plan workflows
|
|
37
|
+
- thread history in `~/.pi/agent/toji-comms/toji-comms.sqlite`
|
|
38
|
+
- commands for model, mode, thread, and plan control
|
|
39
|
+
|
|
40
|
+
## Install from Pi
|
|
41
|
+
|
|
42
|
+
After publishing to npm, install Toji like this:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pi install npm:@mr-jones123/toji
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Try it for one Pi run without installing:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pi -e npm:@mr-jones123/toji
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Local setup
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
bun install
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Use with Pi
|
|
61
|
+
|
|
62
|
+
If installed with `pi install`, start Pi normally and both extensions are discovered.
|
|
63
|
+
|
|
64
|
+
For local development, start Pi with one extension loaded:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pi -e ./packages/toji-mem/src/index.ts
|
|
68
|
+
pi -e ./packages/toji-comms/src/index.ts
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### `toji-mem` commands
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
/toji-index .
|
|
75
|
+
/toji-query indexProject
|
|
76
|
+
/toji-overview .
|
|
77
|
+
/toji-graph index project
|
|
78
|
+
/toji-blast indexProject
|
|
79
|
+
/toji-bench .
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
| command | purpose |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `/toji-index [path]` | index a project into Toji memory |
|
|
85
|
+
| `/toji-query <query>` | search indexed files, symbols, and standards |
|
|
86
|
+
| `/toji-overview [path]` | show compact project overview, expandable in Pi |
|
|
87
|
+
| `/toji-graph <intent>` | find related files/symbols from a natural-language intent |
|
|
88
|
+
| `/toji-blast <symbol>` | traverse likely impact radius for a symbol |
|
|
89
|
+
| `/toji-bench [path]` | run the operational benchmark from inside Pi |
|
|
90
|
+
|
|
91
|
+
### `toji-comms` commands
|
|
92
|
+
|
|
93
|
+
```text
|
|
94
|
+
/toji-comms <message>
|
|
95
|
+
/toji-comms-model <model>
|
|
96
|
+
/toji-comms-mode discuss|ask|critique|decide|verify|plan
|
|
97
|
+
/toji-comms-new [title]
|
|
98
|
+
/toji-plan <message>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
| command | purpose |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `/toji-comms <message>` | send a raw message to the peer model |
|
|
104
|
+
| `/toji-comms-model <model>` | choose the target peer model |
|
|
105
|
+
| `/toji-comms-mode <mode>` | set discussion mode |
|
|
106
|
+
| `/toji-comms-new [title]` | start a new comms thread |
|
|
107
|
+
| `/toji-plan <message>` | compatibility plan command |
|
|
108
|
+
|
|
109
|
+
## Reproduce the operational benchmark
|
|
110
|
+
|
|
111
|
+
The benchmark measures Toji core directly, without LLM/session overhead:
|
|
112
|
+
|
|
113
|
+
- cold index
|
|
114
|
+
- hot re-index
|
|
115
|
+
- query p50
|
|
116
|
+
- project overview p50
|
|
117
|
+
- blast-radius p50
|
|
118
|
+
- indexed files/symbols/edges
|
|
119
|
+
|
|
120
|
+
### 1. Benchmark Toji itself
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
cd packages/toji-mem
|
|
124
|
+
bun run bench --repo . --json
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 2. Benchmark Flask
|
|
128
|
+
|
|
129
|
+
From the repository root:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
mkdir -p benchmarks/repos
|
|
133
|
+
git clone --depth 1 https://github.com/pallets/flask.git benchmarks/repos/flask
|
|
134
|
+
cd packages/toji-mem
|
|
135
|
+
bun run bench --repo ../../benchmarks/repos/flask --json
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
If Flask is already cloned:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
git -C benchmarks/repos/flask pull --ff-only
|
|
142
|
+
cd packages/toji-mem
|
|
143
|
+
bun run bench --repo ../../benchmarks/repos/flask --json
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Current smoke scores
|
|
147
|
+
|
|
148
|
+
| repo | cold index | hot index | query p50 | overview p50 | blast p50 | files | symbols | edges |
|
|
149
|
+
|---|---:|---:|---:|---:|---:|---:|---:|---:|
|
|
150
|
+
| `packages/toji-mem` | 679.02 ms | 5.18 ms | 0.68 ms | 0.75 ms | 1.71 ms | 20 | 98 | 647 |
|
|
151
|
+
| `flask` | 1888.94 ms | 12.85 ms | 0.71 ms | 9.67 ms | 0.29 ms | 83 | 1620 | 7658 |
|
|
152
|
+
|
|
153
|
+
## Benchmark notes
|
|
154
|
+
|
|
155
|
+
- CLI benchmark is the source of truth for stable numbers.
|
|
156
|
+
- `/toji-bench` uses the same benchmark engine from inside Pi.
|
|
157
|
+
- Cloned benchmark repos are scratch data and are not committed.
|
|
158
|
+
- RepoBench validation is planned separately for external retrieval quality.
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mr-jones123/toji",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Toji Pi extensions for code memory and AI-to-AI comms.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi-extension",
|
|
9
|
+
"toji"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"workspaces": [
|
|
13
|
+
"packages/*"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
"packages/toji-mem/src",
|
|
17
|
+
"packages/toji-mem/grammars",
|
|
18
|
+
"packages/toji-comms/src",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"pi": {
|
|
22
|
+
"extensions": [
|
|
23
|
+
"./packages/toji-mem/src/index.ts",
|
|
24
|
+
"./packages/toji-comms/src/index.ts"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"typecheck": "bunx tsc --noEmit",
|
|
29
|
+
"bench": "node benchmarks/run.mjs",
|
|
30
|
+
"toji-mem:typecheck": "cd packages/toji-mem && bun run typecheck"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.18.2",
|
|
34
|
+
"tree-sitter-wasms": "^0.1.13",
|
|
35
|
+
"web-tree-sitter": "^0.25.0",
|
|
36
|
+
"zod": "^4.4.3"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/bun": "latest",
|
|
40
|
+
"typescript": "^5.9.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
44
|
+
"@earendil-works/pi-tui": "*",
|
|
45
|
+
"typebox": "*"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# toji-comms
|
|
2
|
+
|
|
3
|
+
AI-to-AI communication bridge for Pi agents.
|
|
4
|
+
|
|
5
|
+
`toji-comms` is the communication substrate for Toji. It lets the active Pi model compose a message for a peer model, sends it through MCP to `mmx`, stores thread history in SQLite, and returns the peer response.
|
|
6
|
+
|
|
7
|
+
## Tools
|
|
8
|
+
|
|
9
|
+
### `toji_comms_send`
|
|
10
|
+
|
|
11
|
+
Sends an AI-authored message to a peer AI. `userRequest` stores the user's raw request. `sourceMessage` must be written by the active AI and should include interpretation, relevant context, constraints, current findings, and what response is needed from the peer. Calls where `sourceMessage` simply copies `userRequest` are rejected.
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"userRequest": "I want to build X",
|
|
16
|
+
"sourceMessage": "User said they want to build X. My interpretation is...",
|
|
17
|
+
"knownContext": ["Relevant project facts"],
|
|
18
|
+
"questionsForPeer": ["What risks are we missing?"],
|
|
19
|
+
"mode": "discuss",
|
|
20
|
+
"targetModel": "MiniMax-M1"
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Modes:
|
|
25
|
+
|
|
26
|
+
- `discuss`, default
|
|
27
|
+
- `ask`
|
|
28
|
+
- `critique`
|
|
29
|
+
- `decide`
|
|
30
|
+
- `verify`
|
|
31
|
+
- `plan`
|
|
32
|
+
|
|
33
|
+
The result includes the rendered prompt sent to `mmx`, the peer reply, thread id, turn ids, and run metadata.
|
|
34
|
+
|
|
35
|
+
### `toji_plan_run`
|
|
36
|
+
|
|
37
|
+
Compatibility alias for old plan workflows. It sends through `toji-comms` with `mode: "plan"`.
|
|
38
|
+
|
|
39
|
+
## Commands
|
|
40
|
+
|
|
41
|
+
```txt
|
|
42
|
+
/toji-comms <message>
|
|
43
|
+
/toji-comms-model <model>
|
|
44
|
+
/toji-comms-mode discuss|ask|critique|decide|verify|plan
|
|
45
|
+
/toji-comms-new [title]
|
|
46
|
+
/toji-plan <message>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`/toji-comms` is raw mode and sends text directly from the command. Prefer asking the active AI to use `toji_comms_send` for true AI-to-AI context. `/toji-plan` is a compatibility alias that uses plan mode.
|
|
50
|
+
|
|
51
|
+
## Storage
|
|
52
|
+
|
|
53
|
+
SQLite database:
|
|
54
|
+
|
|
55
|
+
```txt
|
|
56
|
+
~/.pi/agent/toji-comms/toji-comms.sqlite
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Tables:
|
|
60
|
+
|
|
61
|
+
- `comms_threads`: long-running AI-to-AI discussion threads
|
|
62
|
+
- `comms_turns`: clean dialogue history, including user, source AI, and peer AI turns
|
|
63
|
+
- `comms_runs`: external model execution metadata for debugging, including the exact rendered prompt sent to the peer model
|
|
64
|
+
|
|
65
|
+
## UI visibility
|
|
66
|
+
|
|
67
|
+
The extension sends visible Pi messages for:
|
|
68
|
+
|
|
69
|
+
1. The user prompt received by `/toji-comms` or `/toji-plan`.
|
|
70
|
+
2. The fully rendered prompt sent to `mmx`, also stored in `comms_runs.rendered_prompt`.
|
|
71
|
+
3. The peer AI reply.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import type { CliAgentInput, CliAgentResult, TargetProvider } from "../comms/types";
|
|
6
|
+
import { runSubprocess } from "./subprocess";
|
|
7
|
+
|
|
8
|
+
export interface CliAgentAdapter {
|
|
9
|
+
provider: TargetProvider;
|
|
10
|
+
buildCommand(input: CliAgentInput): Promise<{ command: string[]; replyFile?: string }> | { command: string[]; replyFile?: string };
|
|
11
|
+
parseReply(result: { stdout: string; stderr: string }, replyFile?: string): Promise<string> | string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const adapters: Record<TargetProvider, CliAgentAdapter> = {
|
|
15
|
+
mmx: {
|
|
16
|
+
provider: "mmx",
|
|
17
|
+
buildCommand(input) {
|
|
18
|
+
const command = ["mmx", "text", "chat", "--message", input.prompt, "--output", "json"];
|
|
19
|
+
if (input.system) command.push("--system", input.system);
|
|
20
|
+
if (input.model) command.push("--model", input.model);
|
|
21
|
+
return { command };
|
|
22
|
+
},
|
|
23
|
+
parseReply(result) {
|
|
24
|
+
return parseMmxReply(result.stdout) || result.stdout.trim();
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
claude: {
|
|
28
|
+
provider: "claude",
|
|
29
|
+
buildCommand(input) {
|
|
30
|
+
const command = ["claude", "--print", "--output-format", "json", "--no-session-persistence"];
|
|
31
|
+
if (input.system) command.push("--system-prompt", input.system);
|
|
32
|
+
if (input.model) command.push("--model", input.model);
|
|
33
|
+
command.push(input.prompt);
|
|
34
|
+
return { command };
|
|
35
|
+
},
|
|
36
|
+
parseReply(result) {
|
|
37
|
+
return parseClaudeReply(result.stdout) || result.stdout.trim();
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
codex: {
|
|
41
|
+
provider: "codex",
|
|
42
|
+
async buildCommand(input) {
|
|
43
|
+
const dir = await mkdtemp(join(tmpdir(), "toji-codex-"));
|
|
44
|
+
const replyFile = join(dir, "reply.txt");
|
|
45
|
+
const prompt = input.system ? `${input.system}\n\n${input.prompt}` : input.prompt;
|
|
46
|
+
const command = ["codex", "exec", "--cd", input.cwd, "--sandbox", "read-only", "--output-last-message", replyFile];
|
|
47
|
+
if (input.model) command.push("--model", input.model);
|
|
48
|
+
command.push(prompt);
|
|
49
|
+
return { command, replyFile };
|
|
50
|
+
},
|
|
51
|
+
async parseReply(result, replyFile) {
|
|
52
|
+
if (!replyFile) return result.stdout.trim();
|
|
53
|
+
try {
|
|
54
|
+
return (await readFile(replyFile, "utf8")).trim() || result.stdout.trim();
|
|
55
|
+
} finally {
|
|
56
|
+
await rm(dirname(replyFile), { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export async function runCliAgent(provider: TargetProvider, input: CliAgentInput, signal?: AbortSignal): Promise<CliAgentResult> {
|
|
63
|
+
const adapter = adapters[provider];
|
|
64
|
+
const { command, replyFile } = await adapter.buildCommand(input);
|
|
65
|
+
const result = await runSubprocess(command, { cwd: input.cwd, signal });
|
|
66
|
+
const reply = await adapter.parseReply(result, replyFile);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
agent: provider,
|
|
70
|
+
command,
|
|
71
|
+
exitCode: result.exitCode,
|
|
72
|
+
stdout: result.stdout,
|
|
73
|
+
stderr: result.stderr,
|
|
74
|
+
durationMs: result.durationMs,
|
|
75
|
+
reply,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface MmxJsonResponse {
|
|
80
|
+
choices?: Array<{ message?: { content?: string }; text?: string }>;
|
|
81
|
+
output_text?: string;
|
|
82
|
+
text?: string;
|
|
83
|
+
message?: string;
|
|
84
|
+
content?: string | Array<{ type?: string; text?: string }>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseMmxReply(stdout: string): string {
|
|
88
|
+
const trimmed = stdout.trim();
|
|
89
|
+
if (!trimmed) return "";
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(trimmed) as MmxJsonResponse;
|
|
93
|
+
const content = Array.isArray(parsed.content)
|
|
94
|
+
? parsed.content.filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n")
|
|
95
|
+
: parsed.content;
|
|
96
|
+
|
|
97
|
+
return parsed.choices?.[0]?.message?.content
|
|
98
|
+
?? parsed.choices?.[0]?.text
|
|
99
|
+
?? parsed.output_text
|
|
100
|
+
?? parsed.text
|
|
101
|
+
?? parsed.message
|
|
102
|
+
?? content
|
|
103
|
+
?? "";
|
|
104
|
+
} catch {
|
|
105
|
+
return "";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function parseClaudeReply(stdout: string): string {
|
|
110
|
+
const trimmed = stdout.trim();
|
|
111
|
+
if (!trimmed) return "";
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const parsed = JSON.parse(trimmed) as { result?: string; message?: { content?: Array<{ type?: string; text?: string }> } };
|
|
115
|
+
return parsed.result
|
|
116
|
+
?? parsed.message?.content?.filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n")
|
|
117
|
+
?? "";
|
|
118
|
+
} catch {
|
|
119
|
+
return "";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { CliAgentInput, CliAgentResult } from "../comms/types";
|
|
2
|
+
import { runSubprocess } from "./subprocess";
|
|
3
|
+
|
|
4
|
+
interface MmxJsonResponse {
|
|
5
|
+
choices?: Array<{
|
|
6
|
+
message?: {
|
|
7
|
+
content?: string;
|
|
8
|
+
};
|
|
9
|
+
text?: string;
|
|
10
|
+
}>;
|
|
11
|
+
output_text?: string;
|
|
12
|
+
text?: string;
|
|
13
|
+
message?: string;
|
|
14
|
+
content?: string | Array<{ type?: string; text?: string; thinking?: string }>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function runMmx(input: CliAgentInput, signal?: AbortSignal): Promise<CliAgentResult> {
|
|
18
|
+
const command = ["mmx", "text", "chat", "--message", input.prompt, "--output", "json"];
|
|
19
|
+
|
|
20
|
+
if (input.system) {
|
|
21
|
+
command.push("--system", input.system);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (input.model) {
|
|
25
|
+
command.push("--model", input.model);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const result = await runSubprocess(command, { cwd: input.cwd, signal });
|
|
29
|
+
const reply = parseMmxReply(result.stdout) || result.stdout.trim();
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
agent: "mmx",
|
|
33
|
+
command,
|
|
34
|
+
exitCode: result.exitCode,
|
|
35
|
+
stdout: result.stdout,
|
|
36
|
+
stderr: result.stderr,
|
|
37
|
+
durationMs: result.durationMs,
|
|
38
|
+
reply,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseMmxReply(stdout: string): string {
|
|
43
|
+
const trimmed = stdout.trim();
|
|
44
|
+
if (!trimmed) return "";
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(trimmed) as MmxJsonResponse;
|
|
48
|
+
const content = Array.isArray(parsed.content)
|
|
49
|
+
? parsed.content
|
|
50
|
+
.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
51
|
+
.map((part) => part.text)
|
|
52
|
+
.join("\n")
|
|
53
|
+
: parsed.content;
|
|
54
|
+
|
|
55
|
+
return parsed.choices?.[0]?.message?.content
|
|
56
|
+
?? parsed.choices?.[0]?.text
|
|
57
|
+
?? parsed.output_text
|
|
58
|
+
?? parsed.text
|
|
59
|
+
?? parsed.message
|
|
60
|
+
?? content
|
|
61
|
+
?? "";
|
|
62
|
+
} catch {
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export interface SubprocessResult {
|
|
4
|
+
command: string[];
|
|
5
|
+
exitCode: number | null;
|
|
6
|
+
stdout: string;
|
|
7
|
+
stderr: string;
|
|
8
|
+
durationMs: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function runSubprocess(command: string[], options: { cwd: string; signal?: AbortSignal }): Promise<SubprocessResult> {
|
|
12
|
+
const [executable, ...args] = command;
|
|
13
|
+
if (!executable) throw new Error("Command must include an executable.");
|
|
14
|
+
|
|
15
|
+
const startedAt = Date.now();
|
|
16
|
+
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const child = spawn(executable, args, {
|
|
19
|
+
cwd: options.cwd,
|
|
20
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
21
|
+
signal: options.signal,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
let stdout = "";
|
|
25
|
+
let stderr = "";
|
|
26
|
+
|
|
27
|
+
child.stdout.setEncoding("utf8");
|
|
28
|
+
child.stderr.setEncoding("utf8");
|
|
29
|
+
child.stdout.on("data", (chunk: string) => {
|
|
30
|
+
stdout += chunk;
|
|
31
|
+
});
|
|
32
|
+
child.stderr.on("data", (chunk: string) => {
|
|
33
|
+
stderr += chunk;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
child.on("error", reject);
|
|
37
|
+
child.on("close", (exitCode) => {
|
|
38
|
+
resolve({
|
|
39
|
+
command,
|
|
40
|
+
exitCode,
|
|
41
|
+
stdout,
|
|
42
|
+
stderr,
|
|
43
|
+
durationMs: Date.now() - startedAt,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { runCliAgent } from "../cli/agents";
|
|
2
|
+
import type { TojiCommsDatabase } from "../db/connection";
|
|
3
|
+
import { renderPeerPrompt } from "./prompt";
|
|
4
|
+
import { addTurn, getOrCreateThread, getRecentTurns, listThreadContext, listThreadContextByIds, storeRun } from "./store";
|
|
5
|
+
import type { CliAgentResult, CommsRequest, CommsResult } from "./types";
|
|
6
|
+
|
|
7
|
+
export async function sendComms(database: TojiCommsDatabase, request: CommsRequest, signal?: AbortSignal): Promise<CommsResult> {
|
|
8
|
+
const thread = getOrCreateThread(database, request);
|
|
9
|
+
const sourceAgent = request.sourceModel?.id ?? request.sourceModel?.provider ?? "source_ai";
|
|
10
|
+
|
|
11
|
+
if (request.context.userRequest) {
|
|
12
|
+
addTurn(database, {
|
|
13
|
+
threadId: thread.id,
|
|
14
|
+
role: "user",
|
|
15
|
+
sourceAgent: "user",
|
|
16
|
+
content: request.context.userRequest,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const sourceTurnId = addTurn(database, {
|
|
21
|
+
threadId: thread.id,
|
|
22
|
+
role: "source_ai",
|
|
23
|
+
sourceAgent,
|
|
24
|
+
provider: request.sourceModel?.provider,
|
|
25
|
+
model: request.sourceModel?.id,
|
|
26
|
+
content: request.context.sourceMessage,
|
|
27
|
+
contextJson: request.context,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const recentTurns = getRecentTurns(database, thread.id, 10);
|
|
31
|
+
const threadContexts = request.includeThreadContext === false
|
|
32
|
+
? []
|
|
33
|
+
: request.contextIds?.length
|
|
34
|
+
? listThreadContextByIds(database, thread.id, request.contextIds)
|
|
35
|
+
: listThreadContext(database, thread.id, 10);
|
|
36
|
+
const prompt = renderPeerPrompt({
|
|
37
|
+
mode: request.mode ?? thread.mode ?? "discuss",
|
|
38
|
+
thread,
|
|
39
|
+
recentTurns,
|
|
40
|
+
threadContexts,
|
|
41
|
+
context: {
|
|
42
|
+
...request.context,
|
|
43
|
+
contextMode: request.context.contextMode ?? ["brief", "thread", "threadContext", "recentTurns"],
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
let run: CliAgentResult;
|
|
48
|
+
try {
|
|
49
|
+
run = await runCliAgent(request.targetProvider ?? "mmx", {
|
|
50
|
+
cwd: request.cwd,
|
|
51
|
+
prompt,
|
|
52
|
+
system: request.system,
|
|
53
|
+
model: request.targetModel,
|
|
54
|
+
}, signal);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
57
|
+
const failedRun: CliAgentResult = {
|
|
58
|
+
agent: request.targetProvider ?? "mmx",
|
|
59
|
+
command: [request.targetProvider ?? "mmx"],
|
|
60
|
+
exitCode: null,
|
|
61
|
+
stdout: "",
|
|
62
|
+
stderr: message,
|
|
63
|
+
durationMs: 0,
|
|
64
|
+
reply: message,
|
|
65
|
+
};
|
|
66
|
+
storeRun(database, thread.id, undefined, failedRun, request.targetModel, prompt);
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const status = run.exitCode === 0 ? "completed" : "failed";
|
|
71
|
+
const provider = request.targetProvider ?? "mmx";
|
|
72
|
+
const reply = run.reply || run.stderr || run.stdout || `${provider} exited with code ${run.exitCode ?? "unknown"}.`;
|
|
73
|
+
const peerTurnId = addTurn(database, {
|
|
74
|
+
threadId: thread.id,
|
|
75
|
+
role: "peer_ai",
|
|
76
|
+
sourceAgent: provider,
|
|
77
|
+
provider,
|
|
78
|
+
model: request.targetModel,
|
|
79
|
+
content: reply,
|
|
80
|
+
});
|
|
81
|
+
storeRun(database, thread.id, peerTurnId, run, request.targetModel, prompt);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
threadId: thread.id,
|
|
85
|
+
sourceTurnId,
|
|
86
|
+
peerTurnId,
|
|
87
|
+
status,
|
|
88
|
+
prompt,
|
|
89
|
+
reply,
|
|
90
|
+
run,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { CommsContextPacket, CommsMode, CommsThread, CommsThreadContext, CommsTurn } from "./types";
|
|
2
|
+
|
|
3
|
+
const TOJI_PROJECT_BRIEF = `Toji is a Pi extension monorepo for agent memory, durable execution goals, and AI-to-AI communication.
|
|
4
|
+
- toji-mem: SQLite + FTS5 + Tree-sitter WASM code graph memory, standards memory, blast radius, project overview.
|
|
5
|
+
- toji-goal: durable goal contracts and goal-scoped todos that survive reload/compaction through Pi branch replay.
|
|
6
|
+
- toji-comms: AI-to-AI communication through MCP and external peer models. It stores threads, turns, prompts, peer replies, and run metadata in SQLite.
|
|
7
|
+
The user wants toji-comms to be primarily discussion and communication, with planning as one explicit mode.`;
|
|
8
|
+
|
|
9
|
+
export function renderPeerPrompt(input: {
|
|
10
|
+
mode: CommsMode;
|
|
11
|
+
thread: CommsThread;
|
|
12
|
+
recentTurns: CommsTurn[];
|
|
13
|
+
threadContexts: CommsThreadContext[];
|
|
14
|
+
context: CommsContextPacket;
|
|
15
|
+
}): string {
|
|
16
|
+
const contextModes = input.context.contextMode ?? ["brief", "thread", "recentTurns"];
|
|
17
|
+
const sections = [
|
|
18
|
+
"You are a peer AI in an AI-to-AI engineering discussion.",
|
|
19
|
+
`Mode: ${input.mode}`,
|
|
20
|
+
modeInstruction(input.mode),
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
if (contextModes.includes("brief")) sections.push(`Project context:\n${TOJI_PROJECT_BRIEF}`);
|
|
24
|
+
if (contextModes.includes("thread")) sections.push(formatThread(input.thread));
|
|
25
|
+
if (contextModes.includes("threadContext") && input.threadContexts.length > 0) sections.push(`Reusable thread context:\n${formatThreadContexts(input.threadContexts)}`);
|
|
26
|
+
if (contextModes.includes("recentTurns")) sections.push(`Recent message history:\n${formatTurns(input.recentTurns)}`);
|
|
27
|
+
|
|
28
|
+
sections.push(formatCurrentMessage(input.context));
|
|
29
|
+
sections.push("Reply to the source AI. Maintain continuity with the thread. Do not pretend you can inspect files unless context was provided.");
|
|
30
|
+
|
|
31
|
+
return sections.filter(Boolean).join("\n\n");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function modeInstruction(mode: CommsMode): string {
|
|
35
|
+
if (mode === "discuss") {
|
|
36
|
+
return [
|
|
37
|
+
"Do not finalize a plan unless explicitly asked.",
|
|
38
|
+
"Continue the discussion, ask clarifying questions, challenge assumptions, and offer options with tradeoffs.",
|
|
39
|
+
"If context is missing, say what is missing and how the source AI should gather it.",
|
|
40
|
+
].join("\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (mode === "plan") return "Produce a concrete plan only if enough context exists. Otherwise ask for missing context first.";
|
|
44
|
+
if (mode === "critique") return "Critique the source AI's proposal. Focus on flaws, risks, missing context, and better alternatives.";
|
|
45
|
+
if (mode === "decide") return "Recommend a decision with rationale, tradeoffs, and conditions that would change the decision.";
|
|
46
|
+
if (mode === "verify") return "Review the verification approach. Identify gaps, regression risks, and stronger validation steps.";
|
|
47
|
+
return "Answer the source AI's specific questions. Be concise and preserve conversation continuity.";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatThread(thread: CommsThread): string {
|
|
51
|
+
return [
|
|
52
|
+
`Thread: #${thread.id} ${thread.title}`,
|
|
53
|
+
`Status: ${thread.status}`,
|
|
54
|
+
thread.summary ? `Summary: ${thread.summary}` : undefined,
|
|
55
|
+
].filter(Boolean).join("\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatThreadContexts(contexts: CommsThreadContext[]): string {
|
|
59
|
+
return contexts.map((context) => `[${context.type}] #${context.id} ${context.title}\n${context.content}`).join("\n\n");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function formatTurns(turns: CommsTurn[]): string {
|
|
63
|
+
if (turns.length === 0) return "No previous turns in this thread.";
|
|
64
|
+
return turns.map((turn) => `${formatSpeaker(turn)}:\n${turn.content}`).join("\n\n");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function formatSpeaker(turn: CommsTurn): string {
|
|
68
|
+
if (turn.role === "user") return "User → Source AI";
|
|
69
|
+
if (turn.role === "source_ai") return `${turn.source_agent ?? "Source AI"} → Peer AI`;
|
|
70
|
+
if (turn.role === "peer_ai") return `${turn.source_agent ?? "Peer AI"} → Source AI`;
|
|
71
|
+
return "System";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function formatCurrentMessage(context: CommsContextPacket): string {
|
|
75
|
+
const sections = ["Current source AI message to peer:", context.sourceMessage];
|
|
76
|
+
if (context.userRequest) sections.push(`User request:\n${context.userRequest}`);
|
|
77
|
+
if (context.knownContext?.length) sections.push(`Known context:\n${formatList(context.knownContext)}`);
|
|
78
|
+
if (context.questionsForPeer?.length) sections.push(`Questions for peer:\n${formatList(context.questionsForPeer)}`);
|
|
79
|
+
return sections.join("\n\n");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function formatList(items: string[]): string {
|
|
83
|
+
return items.map((item, index) => `${index + 1}. ${item}`).join("\n");
|
|
84
|
+
}
|