@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.
Files changed (42) hide show
  1. package/README.md +158 -0
  2. package/package.json +47 -0
  3. package/packages/toji-comms/README.md +71 -0
  4. package/packages/toji-comms/src/cli/agents.ts +121 -0
  5. package/packages/toji-comms/src/cli/mmx.ts +65 -0
  6. package/packages/toji-comms/src/cli/subprocess.ts +47 -0
  7. package/packages/toji-comms/src/comms/orchestrator.ts +92 -0
  8. package/packages/toji-comms/src/comms/prompt.ts +84 -0
  9. package/packages/toji-comms/src/comms/store.ts +145 -0
  10. package/packages/toji-comms/src/comms/types.ts +94 -0
  11. package/packages/toji-comms/src/db/connection.ts +58 -0
  12. package/packages/toji-comms/src/db/migrations.ts +69 -0
  13. package/packages/toji-comms/src/index.ts +368 -0
  14. package/packages/toji-comms/src/mcp/client.ts +71 -0
  15. package/packages/toji-comms/src/mcp/server.ts +81 -0
  16. package/packages/toji-mem/README.md +52 -0
  17. package/packages/toji-mem/grammars/manifest.json +9 -0
  18. package/packages/toji-mem/grammars/tree-sitter-cpp.wasm +0 -0
  19. package/packages/toji-mem/grammars/tree-sitter-dart.wasm +0 -0
  20. package/packages/toji-mem/grammars/tree-sitter-java.wasm +0 -0
  21. package/packages/toji-mem/grammars/tree-sitter-javascript.wasm +0 -0
  22. package/packages/toji-mem/grammars/tree-sitter-python.wasm +0 -0
  23. package/packages/toji-mem/grammars/tree-sitter-tsx.wasm +0 -0
  24. package/packages/toji-mem/grammars/tree-sitter-typescript.wasm +0 -0
  25. package/packages/toji-mem/src/db/connection.ts +58 -0
  26. package/packages/toji-mem/src/db/migrations.ts +181 -0
  27. package/packages/toji-mem/src/index.ts +326 -0
  28. package/packages/toji-mem/src/indexer/file-walker.ts +45 -0
  29. package/packages/toji-mem/src/indexer/index-project.ts +277 -0
  30. package/packages/toji-mem/src/indexer/parsers/cpp.ts +81 -0
  31. package/packages/toji-mem/src/indexer/parsers/dart.ts +91 -0
  32. package/packages/toji-mem/src/indexer/parsers/java.ts +83 -0
  33. package/packages/toji-mem/src/indexer/parsers/python.ts +84 -0
  34. package/packages/toji-mem/src/indexer/parsers/registry.ts +28 -0
  35. package/packages/toji-mem/src/indexer/parsers/tree-sitter-loader.ts +39 -0
  36. package/packages/toji-mem/src/indexer/parsers/types.ts +48 -0
  37. package/packages/toji-mem/src/indexer/parsers/typescript.ts +105 -0
  38. package/packages/toji-mem/src/standards/store.ts +52 -0
  39. package/packages/toji-mem/src/tools/blast-radius.ts +98 -0
  40. package/packages/toji-mem/src/tools/graph-explore.ts +186 -0
  41. package/packages/toji-mem/src/tools/project-overview.ts +102 -0
  42. package/packages/toji-mem/src/tools/query-memory.ts +105 -0
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # Toji
2
+
3
+ ![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178c6?logo=typescript&logoColor=white)
4
+ ![Bun](https://img.shields.io/badge/runtime-Bun-black?logo=bun&logoColor=white)
5
+ ![SQLite](https://img.shields.io/badge/storage-SQLite%20FTS5-003b57?logo=sqlite&logoColor=white)
6
+ ![Pi Extension](https://img.shields.io/badge/Pi-extension-7c3aed)
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
+ }