aos-harness 0.4.1 → 0.5.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/adapters/claude-code/package.json +15 -4
- package/adapters/claude-code/src/agent-runtime.ts +28 -0
- package/adapters/claude-code/src/index.ts +1 -1
- package/adapters/codex/README.md +14 -0
- package/adapters/codex/package.json +15 -4
- package/adapters/codex/src/agent-runtime.ts +22 -0
- package/adapters/gemini/package.json +15 -4
- package/adapters/gemini/src/agent-runtime.ts +52 -0
- package/adapters/gemini/src/index.ts +1 -1
- package/adapters/pi/package.json +15 -4
- package/adapters/pi/src/index.ts +3 -73
- package/adapters/shared/README.md +15 -0
- package/adapters/shared/package.json +14 -3
- package/adapters/shared/src/agent-discovery.ts +71 -0
- package/adapters/shared/src/index.ts +1 -0
- package/adapters/shared/tests/agent-discovery.test.ts +65 -0
- package/package.json +4 -2
- package/src/adapter-config.ts +16 -0
- package/src/adapter-session.ts +319 -0
- package/src/bridge-server.ts +54 -0
- package/src/commands/run.ts +18 -9
- package/src/gauges.ts +33 -0
- package/src/mcp-arbiter-bridge.ts +95 -0
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aos-harness/claude-code-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "AOS Harness adapter for Anthropic's Claude Code CLI.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/aos-engineer/aos-harness.git",
|
|
9
|
+
"directory": "adapters/claude-code"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://aos.engineer",
|
|
12
|
+
"keywords": ["aos-harness", "ai-agents", "claude-code", "adapter"],
|
|
4
13
|
"type": "module",
|
|
14
|
+
"engines": { "bun": ">=1.0.0" },
|
|
5
15
|
"exports": { ".": "./src/index.ts" },
|
|
6
|
-
"files": ["src/"],
|
|
16
|
+
"files": ["src/", "README.md"],
|
|
17
|
+
"publishConfig": { "access": "public" },
|
|
7
18
|
"dependencies": {
|
|
8
|
-
"@aos-harness/runtime": "0.
|
|
9
|
-
"@aos-harness/adapter-shared": "0.
|
|
19
|
+
"@aos-harness/runtime": "0.5.0",
|
|
20
|
+
"@aos-harness/adapter-shared": "0.5.0"
|
|
10
21
|
},
|
|
11
22
|
"devDependencies": {
|
|
12
23
|
"@types/bun": "latest",
|
|
@@ -17,6 +17,13 @@ import {
|
|
|
17
17
|
} from "@aos-harness/adapter-shared";
|
|
18
18
|
import type { BaseEventBus } from "@aos-harness/adapter-shared";
|
|
19
19
|
|
|
20
|
+
// ── McpBridgeOptions ─────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export interface McpBridgeOptions {
|
|
23
|
+
bridgeScriptPath: string;
|
|
24
|
+
socketPath: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
// ── ClaudeCodeAgentRuntime ────────────────────────────────────────
|
|
21
28
|
|
|
22
29
|
export class ClaudeCodeAgentRuntime extends BaseAgentRuntime {
|
|
@@ -57,6 +64,9 @@ export class ClaudeCodeAgentRuntime extends BaseAgentRuntime {
|
|
|
57
64
|
args.push("--resume", state.sessionFile);
|
|
58
65
|
}
|
|
59
66
|
|
|
67
|
+
// Extra args (e.g. MCP flags) are spliced in before the positional message
|
|
68
|
+
args.push(...(opts?.extraArgs ?? []));
|
|
69
|
+
|
|
60
70
|
// Message is always the final argument
|
|
61
71
|
args.push(message);
|
|
62
72
|
return args;
|
|
@@ -179,4 +189,22 @@ export class ClaudeCodeAgentRuntime extends BaseAgentRuntime {
|
|
|
179
189
|
};
|
|
180
190
|
return pricing[tier];
|
|
181
191
|
}
|
|
192
|
+
|
|
193
|
+
buildMcpArgs(opts: McpBridgeOptions): string[] {
|
|
194
|
+
const config = JSON.stringify({
|
|
195
|
+
mcpServers: {
|
|
196
|
+
aos: {
|
|
197
|
+
command: "bun",
|
|
198
|
+
args: [opts.bridgeScriptPath],
|
|
199
|
+
env: { AOS_BRIDGE_SOCKET: opts.socketPath },
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
return [
|
|
204
|
+
"--mcp-config", config,
|
|
205
|
+
"--strict-mcp-config",
|
|
206
|
+
"--allowedTools", "mcp__aos__delegate mcp__aos__end",
|
|
207
|
+
"--permission-mode", "bypassPermissions",
|
|
208
|
+
];
|
|
209
|
+
}
|
|
182
210
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { ClaudeCodeAgentRuntime } from "./agent-runtime";
|
|
1
|
+
export { ClaudeCodeAgentRuntime, type McpBridgeOptions } from "./agent-runtime";
|
|
2
2
|
export { BaseEventBus, TerminalUI, BaseWorkflow, composeAdapter } from "@aos-harness/adapter-shared";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# @aos-harness/codex-adapter
|
|
2
|
+
|
|
3
|
+
AOS Harness adapter for OpenAI's [Codex CLI](https://github.com/openai/codex). Lets you run AOS deliberation and execution profiles with Codex as the underlying agent runtime.
|
|
4
|
+
|
|
5
|
+
Part of the [AOS Harness](https://aos.engineer) monorepo. Bundled with the `aos-harness` CLI — install standalone only if you want a lean, Codex-only setup.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Bun ≥ 1.0.0
|
|
10
|
+
- Codex CLI installed and authenticated on the host
|
|
11
|
+
|
|
12
|
+
## License
|
|
13
|
+
|
|
14
|
+
MIT
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aos-harness/codex-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "AOS Harness adapter for OpenAI's Codex CLI.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/aos-engineer/aos-harness.git",
|
|
9
|
+
"directory": "adapters/codex"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://aos.engineer",
|
|
12
|
+
"keywords": ["aos-harness", "ai-agents", "codex", "adapter"],
|
|
4
13
|
"type": "module",
|
|
14
|
+
"engines": { "bun": ">=1.0.0" },
|
|
5
15
|
"exports": { ".": "./src/index.ts" },
|
|
6
|
-
"files": ["src/"],
|
|
16
|
+
"files": ["src/", "README.md"],
|
|
17
|
+
"publishConfig": { "access": "public" },
|
|
7
18
|
"dependencies": {
|
|
8
|
-
"@aos-harness/runtime": "0.
|
|
9
|
-
"@aos-harness/adapter-shared": "0.
|
|
19
|
+
"@aos-harness/runtime": "0.5.0",
|
|
20
|
+
"@aos-harness/adapter-shared": "0.5.0"
|
|
10
21
|
},
|
|
11
22
|
"devDependencies": {
|
|
12
23
|
"@types/bun": "latest",
|
|
@@ -17,6 +17,13 @@ import {
|
|
|
17
17
|
} from "@aos-harness/adapter-shared";
|
|
18
18
|
import type { BaseEventBus } from "@aos-harness/adapter-shared";
|
|
19
19
|
|
|
20
|
+
// ── McpBridgeOptions ──────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export interface McpBridgeOptions {
|
|
23
|
+
bridgeScriptPath: string;
|
|
24
|
+
socketPath: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
// ── CodexAgentRuntime ─────────────────────────────────────────────
|
|
21
28
|
|
|
22
29
|
export class CodexAgentRuntime extends BaseAgentRuntime {
|
|
@@ -54,6 +61,9 @@ export class CodexAgentRuntime extends BaseAgentRuntime {
|
|
|
54
61
|
args.push("--session", state.sessionFile);
|
|
55
62
|
}
|
|
56
63
|
|
|
64
|
+
// Extra args (e.g. MCP flags) are spliced in before the positional message
|
|
65
|
+
args.push(...(opts?.extraArgs ?? []));
|
|
66
|
+
|
|
57
67
|
// Message is always the final argument
|
|
58
68
|
args.push(message);
|
|
59
69
|
return args;
|
|
@@ -174,6 +184,18 @@ export class CodexAgentRuntime extends BaseAgentRuntime {
|
|
|
174
184
|
return { type: "unknown", metered: false };
|
|
175
185
|
}
|
|
176
186
|
|
|
187
|
+
// Paths are controlled by adapter-session.ts (temp sock in /tmp, script in installed package) — shell-safe characters guaranteed; no escaping needed.
|
|
188
|
+
buildMcpArgs(opts: McpBridgeOptions): string[] {
|
|
189
|
+
return [
|
|
190
|
+
"-c", `mcp_servers.aos.command="bun"`,
|
|
191
|
+
"-c", `mcp_servers.aos.args=["${opts.bridgeScriptPath}"]`,
|
|
192
|
+
"-c", `mcp_servers.aos.env={AOS_BRIDGE_SOCKET="${opts.socketPath}"}`,
|
|
193
|
+
"-c", `mcp_servers.aos.required=true`,
|
|
194
|
+
"-c", `mcp_servers.aos.enabled_tools=["delegate","end"]`,
|
|
195
|
+
"-c", `mcp_servers.aos.tool_timeout_sec=600`,
|
|
196
|
+
];
|
|
197
|
+
}
|
|
198
|
+
|
|
177
199
|
getModelCost(tier: ModelTier): ModelCost {
|
|
178
200
|
const pricing: Record<ModelTier, ModelCost> = {
|
|
179
201
|
economy: {
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aos-harness/gemini-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "AOS Harness adapter for Google's Gemini CLI.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/aos-engineer/aos-harness.git",
|
|
9
|
+
"directory": "adapters/gemini"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://aos.engineer",
|
|
12
|
+
"keywords": ["aos-harness", "ai-agents", "gemini", "adapter"],
|
|
4
13
|
"type": "module",
|
|
14
|
+
"engines": { "bun": ">=1.0.0" },
|
|
5
15
|
"exports": { ".": "./src/index.ts" },
|
|
6
|
-
"files": ["src/"],
|
|
16
|
+
"files": ["src/", "README.md"],
|
|
17
|
+
"publishConfig": { "access": "public" },
|
|
7
18
|
"dependencies": {
|
|
8
|
-
"@aos-harness/runtime": "0.
|
|
9
|
-
"@aos-harness/adapter-shared": "0.
|
|
19
|
+
"@aos-harness/runtime": "0.5.0",
|
|
20
|
+
"@aos-harness/adapter-shared": "0.5.0"
|
|
10
21
|
},
|
|
11
22
|
"devDependencies": {
|
|
12
23
|
"@types/bun": "latest",
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
// Extends BaseAgentRuntime with Gemini CLI integration.
|
|
3
3
|
|
|
4
4
|
import { execSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
export interface McpBridgeOptions {
|
|
7
|
+
bridgeScriptPath: string;
|
|
8
|
+
socketPath: string;
|
|
9
|
+
}
|
|
5
10
|
import type {
|
|
6
11
|
AuthMode,
|
|
7
12
|
ModelCost,
|
|
@@ -54,6 +59,9 @@ export class GeminiAgentRuntime extends BaseAgentRuntime {
|
|
|
54
59
|
args.push("--session", state.sessionFile);
|
|
55
60
|
}
|
|
56
61
|
|
|
62
|
+
// Extra args (e.g. MCP flags) are spliced in before the positional message
|
|
63
|
+
args.push(...(opts?.extraArgs ?? []));
|
|
64
|
+
|
|
57
65
|
// Message is always the final argument
|
|
58
66
|
args.push(message);
|
|
59
67
|
return args;
|
|
@@ -190,4 +198,48 @@ export class GeminiAgentRuntime extends BaseAgentRuntime {
|
|
|
190
198
|
};
|
|
191
199
|
return pricing[tier];
|
|
192
200
|
}
|
|
201
|
+
|
|
202
|
+
buildMcpArgs(): string[] {
|
|
203
|
+
return [
|
|
204
|
+
"--yolo",
|
|
205
|
+
"--allowed-mcp-server-names", "aos",
|
|
206
|
+
];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Writes a project-local .gemini/settings.json with our MCP server config.
|
|
211
|
+
* Returns a restore function that the caller MUST invoke on shutdown to
|
|
212
|
+
* restore any pre-existing settings file.
|
|
213
|
+
*/
|
|
214
|
+
writeMcpSettings(opts: McpBridgeOptions & { projectRoot: string }): () => void {
|
|
215
|
+
const { mkdirSync, writeFileSync, existsSync, renameSync, unlinkSync } = require("node:fs") as typeof import("node:fs");
|
|
216
|
+
const { join } = require("node:path") as typeof import("node:path");
|
|
217
|
+
|
|
218
|
+
const geminiDir = join(opts.projectRoot, ".gemini");
|
|
219
|
+
const settingsPath = join(geminiDir, "settings.json");
|
|
220
|
+
const backupPath = join(geminiDir, "settings.json.aos-backup");
|
|
221
|
+
|
|
222
|
+
mkdirSync(geminiDir, { recursive: true });
|
|
223
|
+
const hadBackup = existsSync(settingsPath);
|
|
224
|
+
if (hadBackup) renameSync(settingsPath, backupPath);
|
|
225
|
+
|
|
226
|
+
writeFileSync(settingsPath, JSON.stringify({
|
|
227
|
+
mcpServers: {
|
|
228
|
+
aos: {
|
|
229
|
+
command: "bun",
|
|
230
|
+
args: [opts.bridgeScriptPath],
|
|
231
|
+
env: { AOS_BRIDGE_SOCKET: opts.socketPath },
|
|
232
|
+
trust: true,
|
|
233
|
+
timeout: 600000,
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
}, null, 2));
|
|
237
|
+
|
|
238
|
+
return () => {
|
|
239
|
+
try { unlinkSync(settingsPath); } catch { /* ignore */ }
|
|
240
|
+
if (hadBackup) {
|
|
241
|
+
try { renameSync(backupPath, settingsPath); } catch { /* ignore */ }
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
193
245
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { GeminiAgentRuntime } from "./agent-runtime";
|
|
1
|
+
export { GeminiAgentRuntime, type McpBridgeOptions } from "./agent-runtime";
|
|
2
2
|
export { BaseEventBus, TerminalUI, BaseWorkflow, composeAdapter } from "@aos-harness/adapter-shared";
|
package/adapters/pi/package.json
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aos-harness/pi-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "AOS Harness adapter for the Pi coding agent runtime.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/aos-engineer/aos-harness.git",
|
|
9
|
+
"directory": "adapters/pi"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://aos.engineer",
|
|
12
|
+
"keywords": ["aos-harness", "ai-agents", "pi", "adapter"],
|
|
4
13
|
"type": "module",
|
|
14
|
+
"engines": { "bun": ">=1.0.0" },
|
|
5
15
|
"pi": {
|
|
6
16
|
"extensions": ["./src/index.ts"]
|
|
7
17
|
},
|
|
8
18
|
"exports": { ".": "./src/index.ts" },
|
|
9
|
-
"files": ["src/"],
|
|
19
|
+
"files": ["src/", "README.md"],
|
|
20
|
+
"publishConfig": { "access": "public" },
|
|
10
21
|
"dependencies": {
|
|
11
|
-
"@aos-harness/adapter-shared": "0.
|
|
12
|
-
"@aos-harness/runtime": "0.
|
|
22
|
+
"@aos-harness/adapter-shared": "0.5.0",
|
|
23
|
+
"@aos-harness/runtime": "0.5.0",
|
|
13
24
|
"js-yaml": "^4.1.0"
|
|
14
25
|
},
|
|
15
26
|
"devDependencies": {
|
package/adapters/pi/src/index.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// Wires all 4 adapter layers together and makes the AOS Harness
|
|
3
3
|
// runnable as a Pi extension.
|
|
4
4
|
|
|
5
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, statSync
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, statSync } from "node:fs";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
|
-
import { join, dirname, basename
|
|
7
|
+
import { join, dirname, basename } from "node:path";
|
|
8
8
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
9
|
import { Text, truncateToWidth } from "@mariozechner/pi-tui";
|
|
10
10
|
import { Type } from "@sinclair/typebox";
|
|
@@ -12,8 +12,7 @@ import { Type } from "@sinclair/typebox";
|
|
|
12
12
|
import { PiAgentRuntime } from "./agent-runtime";
|
|
13
13
|
import { PiEventBus } from "./event-bus";
|
|
14
14
|
import { PiUI } from "./ui";
|
|
15
|
-
import { BaseWorkflow } from "@aos-harness/adapter-shared";
|
|
16
|
-
import { composeAdapter } from "@aos-harness/adapter-shared";
|
|
15
|
+
import { BaseWorkflow, composeAdapter, discoverAgents, createFlatAgentsDir, findProjectRoot } from "@aos-harness/adapter-shared";
|
|
17
16
|
|
|
18
17
|
import { AOSEngine } from "@aos-harness/runtime";
|
|
19
18
|
import type { AOSAdapter, ConstraintState, ProfileConfig } from "@aos-harness/runtime/types";
|
|
@@ -22,75 +21,6 @@ import { validateBrief } from "@aos-harness/runtime/config-loader";
|
|
|
22
21
|
|
|
23
22
|
// ── Helpers ─────────────────────────────────────────────────────
|
|
24
23
|
|
|
25
|
-
/** Walk up from `cwd` looking for a directory containing `core/`. */
|
|
26
|
-
function findProjectRoot(cwd: string): string | null {
|
|
27
|
-
let dir = resolve(cwd);
|
|
28
|
-
for (let i = 0; i < 20; i++) {
|
|
29
|
-
if (existsSync(join(dir, "core"))) return dir;
|
|
30
|
-
if (existsSync(join(dir, ".aos"))) return dir;
|
|
31
|
-
const parent = dirname(dir);
|
|
32
|
-
if (parent === dir) break;
|
|
33
|
-
dir = parent;
|
|
34
|
-
}
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Recursively discover all agent directories (those containing agent.yaml).
|
|
40
|
-
* Returns a Map of agentId -> absolute directory path.
|
|
41
|
-
*/
|
|
42
|
-
function discoverAgents(agentsDir: string): Map<string, string> {
|
|
43
|
-
const agents = new Map<string, string>();
|
|
44
|
-
|
|
45
|
-
function walk(dir: string): void {
|
|
46
|
-
if (!existsSync(dir)) return;
|
|
47
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
48
|
-
for (const entry of entries) {
|
|
49
|
-
if (!entry.isDirectory()) continue;
|
|
50
|
-
const subDir = join(dir, entry.name);
|
|
51
|
-
const yamlPath = join(subDir, "agent.yaml");
|
|
52
|
-
if (existsSync(yamlPath)) {
|
|
53
|
-
// Read the id from agent.yaml
|
|
54
|
-
try {
|
|
55
|
-
const raw = readFileSync(yamlPath, "utf-8");
|
|
56
|
-
const idMatch = raw.match(/^id:\s*(.+)$/m);
|
|
57
|
-
if (idMatch) {
|
|
58
|
-
agents.set(idMatch[1].trim(), subDir);
|
|
59
|
-
}
|
|
60
|
-
} catch {
|
|
61
|
-
// Skip unreadable
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
// Recurse into subdirectories
|
|
65
|
-
walk(subDir);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
walk(agentsDir);
|
|
70
|
-
return agents;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Create a flat temporary directory with symlinks so the engine can
|
|
75
|
-
* resolve agent IDs via `join(agentsDir, id)`.
|
|
76
|
-
*/
|
|
77
|
-
function createFlatAgentsDir(projectRoot: string, agentMap: Map<string, string>): string {
|
|
78
|
-
const flatDir = join(projectRoot, ".aos", "_flat_agents");
|
|
79
|
-
if (existsSync(flatDir)) {
|
|
80
|
-
rmSync(flatDir, { recursive: true, force: true });
|
|
81
|
-
}
|
|
82
|
-
mkdirSync(flatDir, { recursive: true });
|
|
83
|
-
|
|
84
|
-
for (const [id, dirPath] of agentMap) {
|
|
85
|
-
const linkPath = join(flatDir, id);
|
|
86
|
-
if (!existsSync(linkPath)) {
|
|
87
|
-
symlinkSync(dirPath, linkPath, "dir");
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return flatDir;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
24
|
/** List subdirectories that contain a given file. */
|
|
95
25
|
function listDirsWithFile(parentDir: string, fileName: string): { name: string; dir: string; mtime: number }[] {
|
|
96
26
|
if (!existsSync(parentDir)) return [];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# @aos-harness/adapter-shared
|
|
2
|
+
|
|
3
|
+
Shared base classes and utilities used by every AOS Harness platform adapter (Claude Code, Codex, Gemini, Pi).
|
|
4
|
+
|
|
5
|
+
Re-exports common types (`AgentRuntime`, `EventBus`, etc.) and provides composition helpers (`composeAdapter`, `BaseEventBus`, `BaseWorkflow`) so each adapter only needs to implement platform-specific runtime logic.
|
|
6
|
+
|
|
7
|
+
Part of the [AOS Harness](https://aos.engineer) monorepo. Most users install `aos-harness` (the CLI) instead of consuming this package directly.
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- Bun ≥ 1.0.0
|
|
12
|
+
|
|
13
|
+
## License
|
|
14
|
+
|
|
15
|
+
MIT
|
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aos-harness/adapter-shared",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Shared base classes and utilities for AOS Harness platform adapters.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/aos-engineer/aos-harness.git",
|
|
9
|
+
"directory": "adapters/shared"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://aos.engineer",
|
|
12
|
+
"keywords": ["aos-harness", "ai-agents", "adapter", "shared"],
|
|
4
13
|
"type": "module",
|
|
14
|
+
"engines": { "bun": ">=1.0.0" },
|
|
5
15
|
"exports": {
|
|
6
16
|
".": "./src/index.ts",
|
|
7
17
|
"./*": "./src/*"
|
|
8
18
|
},
|
|
9
|
-
"files": ["src/"],
|
|
19
|
+
"files": ["src/", "README.md"],
|
|
20
|
+
"publishConfig": { "access": "public" },
|
|
10
21
|
"dependencies": {
|
|
11
|
-
"@aos-harness/runtime": "0.
|
|
22
|
+
"@aos-harness/runtime": "0.5.0",
|
|
12
23
|
"js-yaml": "^4.1.0"
|
|
13
24
|
},
|
|
14
25
|
"devDependencies": {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, mkdirSync, symlinkSync, rmSync } from "node:fs";
|
|
2
|
+
import { join, dirname, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
/** Walk up from `cwd` looking for a directory containing `core/`. */
|
|
5
|
+
export function findProjectRoot(cwd: string): string | null {
|
|
6
|
+
let dir = resolve(cwd);
|
|
7
|
+
for (let i = 0; i < 20; i++) {
|
|
8
|
+
if (existsSync(join(dir, "core"))) return dir;
|
|
9
|
+
if (existsSync(join(dir, ".aos"))) return dir;
|
|
10
|
+
const parent = dirname(dir);
|
|
11
|
+
if (parent === dir) break;
|
|
12
|
+
dir = parent;
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Recursively discover all agent directories (those containing agent.yaml).
|
|
19
|
+
* Returns a Map of agentId -> absolute directory path.
|
|
20
|
+
*/
|
|
21
|
+
export function discoverAgents(agentsDir: string): Map<string, string> {
|
|
22
|
+
const agents = new Map<string, string>();
|
|
23
|
+
|
|
24
|
+
function walk(dir: string): void {
|
|
25
|
+
if (!existsSync(dir)) return;
|
|
26
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (!entry.isDirectory()) continue;
|
|
29
|
+
const subDir = join(dir, entry.name);
|
|
30
|
+
const yamlPath = join(subDir, "agent.yaml");
|
|
31
|
+
if (existsSync(yamlPath)) {
|
|
32
|
+
// Read the id from agent.yaml
|
|
33
|
+
try {
|
|
34
|
+
const raw = readFileSync(yamlPath, "utf-8");
|
|
35
|
+
const idMatch = raw.match(/^id:\s*(.+)$/m);
|
|
36
|
+
if (idMatch) {
|
|
37
|
+
agents.set(idMatch[1].trim(), subDir);
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// Skip unreadable
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Recurse into subdirectories
|
|
44
|
+
walk(subDir);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
walk(agentsDir);
|
|
49
|
+
return agents;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a flat temporary directory with symlinks so the engine can
|
|
54
|
+
* resolve agent IDs via `join(agentsDir, id)`.
|
|
55
|
+
*/
|
|
56
|
+
export function createFlatAgentsDir(projectRoot: string, agentMap: Map<string, string>): string {
|
|
57
|
+
const flatDir = join(projectRoot, ".aos", "_flat_agents");
|
|
58
|
+
if (existsSync(flatDir)) {
|
|
59
|
+
rmSync(flatDir, { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
mkdirSync(flatDir, { recursive: true });
|
|
62
|
+
|
|
63
|
+
for (const [id, dirPath] of agentMap) {
|
|
64
|
+
const linkPath = join(flatDir, id);
|
|
65
|
+
if (!existsSync(linkPath)) {
|
|
66
|
+
symlinkSync(dirPath, linkPath, "dir");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return flatDir;
|
|
71
|
+
}
|
|
@@ -9,3 +9,4 @@ export { BaseEventBus } from "./base-event-bus";
|
|
|
9
9
|
export { TerminalUI } from "./terminal-ui";
|
|
10
10
|
export { BaseWorkflow } from "./base-workflow";
|
|
11
11
|
export { composeAdapter } from "./compose";
|
|
12
|
+
export { discoverAgents, createFlatAgentsDir, findProjectRoot } from "./agent-discovery";
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { test, expect } from "bun:test";
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, lstatSync, realpathSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { discoverAgents, findProjectRoot, createFlatAgentsDir } from "../src/agent-discovery";
|
|
6
|
+
|
|
7
|
+
test("discoverAgents finds agents recursively by agent.yaml", () => {
|
|
8
|
+
const root = mkdtempSync(join(tmpdir(), "discover-"));
|
|
9
|
+
mkdirSync(join(root, "alice"), { recursive: true });
|
|
10
|
+
writeFileSync(join(root, "alice", "agent.yaml"), "id: alice\n");
|
|
11
|
+
mkdirSync(join(root, "nested", "bob"), { recursive: true });
|
|
12
|
+
writeFileSync(join(root, "nested", "bob", "agent.yaml"), "id: bob\n");
|
|
13
|
+
|
|
14
|
+
const map = discoverAgents(root);
|
|
15
|
+
expect(map.get("alice")).toBe(join(root, "alice"));
|
|
16
|
+
expect(map.get("bob")).toBe(join(root, "nested", "bob"));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("findProjectRoot walks up to find core/ or .aos/", () => {
|
|
20
|
+
const root = mkdtempSync(join(tmpdir(), "proj-"));
|
|
21
|
+
mkdirSync(join(root, "core"));
|
|
22
|
+
const deep = join(root, "a", "b", "c");
|
|
23
|
+
mkdirSync(deep, { recursive: true });
|
|
24
|
+
expect(findProjectRoot(deep)).toBe(root);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("createFlatAgentsDir creates symlinks and wipes on re-call", () => {
|
|
28
|
+
// Set up a projectRoot temp dir
|
|
29
|
+
const projectRoot = mkdtempSync(join(tmpdir(), "flat-agents-"));
|
|
30
|
+
|
|
31
|
+
// Create two agent dirs
|
|
32
|
+
const agentAliceDir = mkdtempSync(join(tmpdir(), "agent-alice-"));
|
|
33
|
+
const agentBobDir = mkdtempSync(join(tmpdir(), "agent-bob-"));
|
|
34
|
+
|
|
35
|
+
const agentMap = new Map<string, string>([
|
|
36
|
+
["alice", agentAliceDir],
|
|
37
|
+
["bob", agentBobDir],
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
// First call
|
|
41
|
+
const flatDir = createFlatAgentsDir(projectRoot, agentMap);
|
|
42
|
+
|
|
43
|
+
// Assert returned path
|
|
44
|
+
expect(flatDir).toBe(join(projectRoot, ".aos", "_flat_agents"));
|
|
45
|
+
|
|
46
|
+
// Assert symlinks exist and resolve correctly
|
|
47
|
+
const aliceLink = join(flatDir, "alice");
|
|
48
|
+
const bobLink = join(flatDir, "bob");
|
|
49
|
+
|
|
50
|
+
expect(lstatSync(aliceLink).isSymbolicLink()).toBe(true);
|
|
51
|
+
expect(lstatSync(bobLink).isSymbolicLink()).toBe(true);
|
|
52
|
+
expect(realpathSync(aliceLink)).toBe(realpathSync(agentAliceDir));
|
|
53
|
+
expect(realpathSync(bobLink)).toBe(realpathSync(agentBobDir));
|
|
54
|
+
|
|
55
|
+
// Second call with only bob — alice should be gone (dir wiped+recreated)
|
|
56
|
+
const agentMapReduced = new Map<string, string>([["bob", agentBobDir]]);
|
|
57
|
+
createFlatAgentsDir(projectRoot, agentMapReduced);
|
|
58
|
+
|
|
59
|
+
// alice symlink from the first call must no longer exist
|
|
60
|
+
expect(() => lstatSync(aliceLink)).toThrow();
|
|
61
|
+
|
|
62
|
+
// bob symlink must still be present and valid
|
|
63
|
+
expect(lstatSync(bobLink).isSymbolicLink()).toBe(true);
|
|
64
|
+
expect(realpathSync(bobLink)).toBe(realpathSync(agentBobDir));
|
|
65
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aos-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Agentic Orchestration System — assemble AI agents into deliberation and execution teams",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,7 +36,9 @@
|
|
|
36
36
|
"test": "bun run src/index.ts validate"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@aos-harness/
|
|
39
|
+
"@aos-harness/adapter-shared": "0.5.0",
|
|
40
|
+
"@aos-harness/runtime": "0.5.0",
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
40
42
|
"js-yaml": "^4.1.0"
|
|
41
43
|
},
|
|
42
44
|
"devDependencies": {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
|
|
5
|
+
export interface AdapterConfig {
|
|
6
|
+
platform?: string;
|
|
7
|
+
model_overrides?: Partial<Record<string, string>>;
|
|
8
|
+
theme?: string;
|
|
9
|
+
editor?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function readAdapterConfig(root: string): AdapterConfig | null {
|
|
13
|
+
const p = join(root, ".aos", "adapter.yaml");
|
|
14
|
+
if (!existsSync(p)) return null;
|
|
15
|
+
return yaml.load(readFileSync(p, "utf-8")) as AdapterConfig;
|
|
16
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
// ── adapter-session.ts ────────────────────────────────────────────
|
|
2
|
+
// Orchestration entrypoint used by the CLI to run a deliberation
|
|
3
|
+
// session against one of the non-Pi adapters (Claude Code, Gemini, Codex).
|
|
4
|
+
//
|
|
5
|
+
// Responsibilities:
|
|
6
|
+
// 1. Dynamically load the chosen AgentRuntime class.
|
|
7
|
+
// 2. Compose the 4 adapter layers into an AOSAdapter.
|
|
8
|
+
// 3. Build the AOSEngine and kick off with the brief.
|
|
9
|
+
// 4. Start a Unix-socket bridge that forwards MCP `delegate`/`end`
|
|
10
|
+
// tool calls from the arbiter process into the engine.
|
|
11
|
+
// 5. Resolve the arbiter prompt template and spawn the arbiter,
|
|
12
|
+
// threading MCP CLI flags through MessageOpts.extraArgs.
|
|
13
|
+
// 6. Run a readline loop for `/aos-*` interactive commands.
|
|
14
|
+
|
|
15
|
+
import { join, dirname } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
import readline from "node:readline";
|
|
20
|
+
import {
|
|
21
|
+
BaseEventBus,
|
|
22
|
+
TerminalUI,
|
|
23
|
+
BaseWorkflow,
|
|
24
|
+
composeAdapter,
|
|
25
|
+
discoverAgents,
|
|
26
|
+
createFlatAgentsDir,
|
|
27
|
+
} from "@aos-harness/adapter-shared";
|
|
28
|
+
import { AOSEngine } from "@aos-harness/runtime";
|
|
29
|
+
import { loadAgent } from "@aos-harness/runtime/config-loader";
|
|
30
|
+
import { resolveTemplate } from "@aos-harness/runtime/template-resolver";
|
|
31
|
+
import { startBridgeServer } from "./bridge-server";
|
|
32
|
+
import { renderTextGauge, renderRoundOneLiner } from "./gauges";
|
|
33
|
+
|
|
34
|
+
export interface AdapterSessionConfig {
|
|
35
|
+
platform: string; // "claude-code" | "gemini" | "codex"
|
|
36
|
+
profileDir: string;
|
|
37
|
+
briefPath: string;
|
|
38
|
+
domainName: string | null;
|
|
39
|
+
root: string;
|
|
40
|
+
sessionId: string;
|
|
41
|
+
deliberationDir: string;
|
|
42
|
+
verbose: boolean;
|
|
43
|
+
workflowConfig: any | null;
|
|
44
|
+
workflowsDir: string;
|
|
45
|
+
modelOverrides?: Partial<Record<string, string>>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const ADAPTER_MAP: Record<string, { package: string; className: string }> = {
|
|
49
|
+
"claude-code": {
|
|
50
|
+
package: "@aos-harness/claude-code-adapter",
|
|
51
|
+
className: "ClaudeCodeAgentRuntime",
|
|
52
|
+
},
|
|
53
|
+
gemini: {
|
|
54
|
+
package: "@aos-harness/gemini-adapter",
|
|
55
|
+
className: "GeminiAgentRuntime",
|
|
56
|
+
},
|
|
57
|
+
codex: {
|
|
58
|
+
package: "@aos-harness/codex-adapter",
|
|
59
|
+
className: "CodexAgentRuntime",
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
async function loadAdapterRuntime(platform: string): Promise<any> {
|
|
64
|
+
const entry = ADAPTER_MAP[platform];
|
|
65
|
+
if (!entry) throw new Error(`Unknown adapter: ${platform}`);
|
|
66
|
+
|
|
67
|
+
async function readAdapterVersion(fromPath: string): Promise<string> {
|
|
68
|
+
try {
|
|
69
|
+
const pkgUrl = new URL("../package.json", fromPath).href;
|
|
70
|
+
const { readFile } = await import("node:fs/promises");
|
|
71
|
+
const raw = await readFile(new URL(pkgUrl), "utf-8");
|
|
72
|
+
return (JSON.parse(raw) as { version: string }).version ?? "unknown";
|
|
73
|
+
} catch {
|
|
74
|
+
return "unknown";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const mod = await import(entry.package);
|
|
80
|
+
const resolved = (import.meta as any).resolve?.(entry.package) ?? entry.package;
|
|
81
|
+
const version = await readAdapterVersion(resolved);
|
|
82
|
+
console.error(`[adapter] loaded ${entry.package}@${version} (standalone)`);
|
|
83
|
+
return mod[entry.className];
|
|
84
|
+
} catch {
|
|
85
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
86
|
+
const fallback = join(here, "..", "..", "adapters", platform, "src", "index.ts");
|
|
87
|
+
const mod = await import(fallback);
|
|
88
|
+
const version = await readAdapterVersion(`file://${fallback}`);
|
|
89
|
+
console.error(`[adapter] loaded ${entry.package}@${version} (bundled: ${fallback})`);
|
|
90
|
+
return mod[entry.className];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function toolNamesForPlatform(platform: string): { delegate: string; end: string } {
|
|
95
|
+
if (platform === "claude-code") {
|
|
96
|
+
return { delegate: "mcp__aos__delegate", end: "mcp__aos__end" };
|
|
97
|
+
}
|
|
98
|
+
return { delegate: "delegate", end: "end" };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function runAdapterSession(config: AdapterSessionConfig): Promise<void> {
|
|
102
|
+
const log = (msg: string) => {
|
|
103
|
+
if (config.verbose) console.error(`[session] ${msg}`);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
log("loading adapter runtime");
|
|
107
|
+
const RuntimeClass = await loadAdapterRuntime(config.platform);
|
|
108
|
+
|
|
109
|
+
// ── Layer composition ──────────────────────────────────────
|
|
110
|
+
const eventBus = new BaseEventBus();
|
|
111
|
+
const agentRuntime = new RuntimeClass(eventBus, config.modelOverrides);
|
|
112
|
+
const ui = new TerminalUI();
|
|
113
|
+
const workflow = new BaseWorkflow(agentRuntime, config.root);
|
|
114
|
+
const adapter = composeAdapter(agentRuntime, eventBus, ui, workflow);
|
|
115
|
+
log("layers composed");
|
|
116
|
+
|
|
117
|
+
// ── Agent discovery (flatten nested core/agents layout) ───
|
|
118
|
+
const agentsDir = join(config.root, "core", "agents");
|
|
119
|
+
const agentMap = discoverAgents(agentsDir);
|
|
120
|
+
const flatAgentsDir = createFlatAgentsDir(config.root, agentMap);
|
|
121
|
+
const domainsDir = join(config.root, "core", "domains");
|
|
122
|
+
log(`agents discovered (${agentMap.size})`);
|
|
123
|
+
|
|
124
|
+
// ── Engine setup & brief intake ────────────────────────────
|
|
125
|
+
const engine = new AOSEngine(adapter, config.profileDir, {
|
|
126
|
+
agentsDir: flatAgentsDir,
|
|
127
|
+
domain: config.domainName ?? undefined,
|
|
128
|
+
domainDir: config.domainName ? domainsDir : undefined,
|
|
129
|
+
});
|
|
130
|
+
log("starting engine");
|
|
131
|
+
await engine.start(config.briefPath);
|
|
132
|
+
log("engine started");
|
|
133
|
+
|
|
134
|
+
// ── Bridge server (MCP tool calls → engine) ────────────────
|
|
135
|
+
const sockPath = join(tmpdir(), `aos-bridge-${config.sessionId}.sock`);
|
|
136
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
137
|
+
const bridgeScriptPath = join(here, "mcp-arbiter-bridge.ts");
|
|
138
|
+
|
|
139
|
+
let halted = false;
|
|
140
|
+
const steerQueue: string[] = [];
|
|
141
|
+
|
|
142
|
+
async function waitWhileHalted(): Promise<void> {
|
|
143
|
+
while (halted) await new Promise((r) => setTimeout(r, 200));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function drainSteer(): string {
|
|
147
|
+
if (steerQueue.length === 0) return "";
|
|
148
|
+
const msgs = steerQueue.splice(0);
|
|
149
|
+
return `\n\n[user steer]\n${msgs.join("\n")}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
log(`starting bridge server sock=${sockPath}`);
|
|
153
|
+
const closeBridge = await startBridgeServer(sockPath, {
|
|
154
|
+
delegate: async (params) => {
|
|
155
|
+
await waitWhileHalted();
|
|
156
|
+
const steer = drainSteer();
|
|
157
|
+
const responses = await engine.delegateMessage(
|
|
158
|
+
params.to as any,
|
|
159
|
+
(params.message as string) + steer,
|
|
160
|
+
);
|
|
161
|
+
const cs = engine.getConstraintState();
|
|
162
|
+
process.stdout.write(
|
|
163
|
+
"\n" +
|
|
164
|
+
renderRoundOneLiner({
|
|
165
|
+
round: cs.rounds_completed,
|
|
166
|
+
maxRounds: 8,
|
|
167
|
+
minutes: cs.elapsed_minutes,
|
|
168
|
+
dollars: cs.budget_spent,
|
|
169
|
+
}) +
|
|
170
|
+
"\n",
|
|
171
|
+
);
|
|
172
|
+
return { responses, constraints: cs };
|
|
173
|
+
},
|
|
174
|
+
end: async (params) => {
|
|
175
|
+
await waitWhileHalted();
|
|
176
|
+
const responses = await engine.end(params.closing_message as string);
|
|
177
|
+
return { ok: true, responses };
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
log("bridge listening");
|
|
182
|
+
// ── Arbiter prompt resolution ──────────────────────────────
|
|
183
|
+
const arbiterDir = agentMap.get("arbiter");
|
|
184
|
+
if (!arbiterDir) throw new Error("No arbiter agent found in core/agents/");
|
|
185
|
+
|
|
186
|
+
const promptPath = join(arbiterDir, "prompt.md");
|
|
187
|
+
const rawPrompt = readFileSync(promptPath, "utf-8");
|
|
188
|
+
const briefContent = readFileSync(config.briefPath, "utf-8");
|
|
189
|
+
const participants = [...agentMap.keys()].filter((id) => id !== "arbiter");
|
|
190
|
+
const memoPath = join(config.deliberationDir, "memo.md");
|
|
191
|
+
const transcriptPath = join(config.deliberationDir, "transcript.jsonl");
|
|
192
|
+
const tools = toolNamesForPlatform(config.platform);
|
|
193
|
+
|
|
194
|
+
const templateVars: Record<string, string> = {
|
|
195
|
+
// Spec-compliant underscore names
|
|
196
|
+
session_id: config.sessionId,
|
|
197
|
+
brief_slug: config.sessionId,
|
|
198
|
+
brief: briefContent,
|
|
199
|
+
format: "brief",
|
|
200
|
+
agent_id: "arbiter",
|
|
201
|
+
agent_name: "Arbiter",
|
|
202
|
+
participants: participants.join(", "),
|
|
203
|
+
constraints: "(see constraint state in tool responses)",
|
|
204
|
+
expertise_block: "",
|
|
205
|
+
output_path: memoPath,
|
|
206
|
+
deliberation_dir: config.deliberationDir,
|
|
207
|
+
transcript_path: transcriptPath,
|
|
208
|
+
delegate_tool: tools.delegate,
|
|
209
|
+
end_tool: tools.end,
|
|
210
|
+
role_override: "",
|
|
211
|
+
// Back-compat hyphenated aliases
|
|
212
|
+
"session-id": config.sessionId,
|
|
213
|
+
"brief-content": briefContent,
|
|
214
|
+
"output-path": memoPath,
|
|
215
|
+
"deliberation-dir": config.deliberationDir,
|
|
216
|
+
"memo-path": memoPath,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const resolvedPrompt = resolveTemplate(rawPrompt, templateVars);
|
|
220
|
+
|
|
221
|
+
// The arbiter prompt was authored when tools were named bare (`delegate`, `end`).
|
|
222
|
+
// Claude Code exposes MCP tools as `mcp__aos__delegate` / `mcp__aos__end`, so the
|
|
223
|
+
// model would attempt to call tools that don't exist. Prepend a short preamble
|
|
224
|
+
// that maps the names it'll see to the names the prompt uses.
|
|
225
|
+
const toolPreamble =
|
|
226
|
+
config.platform === "claude-code"
|
|
227
|
+
? `IMPORTANT — Tool names for this session:\n` +
|
|
228
|
+
`- Where the instructions below say \`delegate(...)\`, call \`${tools.delegate}\` (that is the actual MCP tool name you will see).\n` +
|
|
229
|
+
`- Where they say \`end(...)\`, call \`${tools.end}\`.\n` +
|
|
230
|
+
`- These are the ONLY two tools available to you. Use them exactly as described.\n\n---\n\n`
|
|
231
|
+
: "";
|
|
232
|
+
adapter.setOrchestratorPrompt(toolPreamble + resolvedPrompt);
|
|
233
|
+
|
|
234
|
+
// ── Build MCP args and launch arbiter ──────────────────────
|
|
235
|
+
const mcpOpts = { bridgeScriptPath, socketPath: sockPath };
|
|
236
|
+
const mcpArgs: string[] =
|
|
237
|
+
(agentRuntime as any).buildMcpArgs?.(mcpOpts) ?? [];
|
|
238
|
+
|
|
239
|
+
let restoreGeminiSettings: (() => void) | undefined;
|
|
240
|
+
if ((agentRuntime as any).writeMcpSettings) {
|
|
241
|
+
restoreGeminiSettings = (agentRuntime as any).writeMcpSettings({
|
|
242
|
+
...mcpOpts,
|
|
243
|
+
projectRoot: config.root,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Load the arbiter AgentConfig from disk (uses the flat dir so
|
|
248
|
+
// loadAgent sees the same shape the engine sees). Override the
|
|
249
|
+
// systemPrompt with the resolved template.
|
|
250
|
+
const arbiterFlatDir = join(flatAgentsDir, "arbiter");
|
|
251
|
+
const arbiterConfig = loadAgent(arbiterFlatDir);
|
|
252
|
+
arbiterConfig.systemPrompt = resolvedPrompt;
|
|
253
|
+
|
|
254
|
+
log("spawning arbiter");
|
|
255
|
+
const arbiterHandle = await adapter.spawnAgent(arbiterConfig, config.sessionId);
|
|
256
|
+
|
|
257
|
+
// ── Kickoff message ────────────────────────────────────────
|
|
258
|
+
const kickoff =
|
|
259
|
+
"Read the brief below and begin the multi-agent deliberation. " +
|
|
260
|
+
`Use the \`${tools.delegate}\` tool to engage perspective agents and ` +
|
|
261
|
+
`\`${tools.end}\` when ready to wrap up.\n\n` +
|
|
262
|
+
`---\n\n## Brief\n\n${briefContent}`;
|
|
263
|
+
|
|
264
|
+
// ── Interactive readline commands ──────────────────────────
|
|
265
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
266
|
+
rl.on("line", (input) => {
|
|
267
|
+
const trimmed = input.trim();
|
|
268
|
+
if (!trimmed) return;
|
|
269
|
+
if (trimmed === "/aos-halt") {
|
|
270
|
+
halted = true;
|
|
271
|
+
console.log("Deliberation paused. Type /aos-resume to continue.");
|
|
272
|
+
} else if (trimmed === "/aos-resume") {
|
|
273
|
+
halted = false;
|
|
274
|
+
console.log("Resumed.");
|
|
275
|
+
} else if (trimmed === "/aos-end") {
|
|
276
|
+
steerQueue.push("Please wrap up now and call the end tool.");
|
|
277
|
+
} else if (trimmed === "/aos-status") {
|
|
278
|
+
const cs = engine.getConstraintState();
|
|
279
|
+
console.log(renderTextGauge("TIME", cs.elapsed_minutes, 2, 10, "min"));
|
|
280
|
+
if (cs.metered) {
|
|
281
|
+
console.log(renderTextGauge("BUDGET", cs.budget_spent, 1, 10, "$"));
|
|
282
|
+
}
|
|
283
|
+
console.log(renderTextGauge("ROUNDS", cs.rounds_completed, 2, 8, "rounds"));
|
|
284
|
+
} else if (trimmed.startsWith("/aos-steer ")) {
|
|
285
|
+
steerQueue.push(trimmed.slice("/aos-steer ".length));
|
|
286
|
+
} else if (!trimmed.startsWith("/")) {
|
|
287
|
+
steerQueue.push(trimmed);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
log(`sending kickoff to arbiter (mcpArgs=${mcpArgs.length})`);
|
|
293
|
+
console.log(
|
|
294
|
+
`\nArbiter running (${config.platform}). ` +
|
|
295
|
+
`Tool calls will stream as rounds complete. ` +
|
|
296
|
+
`Bridge socket: ${sockPath}\n`,
|
|
297
|
+
);
|
|
298
|
+
const response = await adapter.sendMessage(arbiterHandle, kickoff, {
|
|
299
|
+
extraArgs: mcpArgs,
|
|
300
|
+
});
|
|
301
|
+
if ((response as any).status !== "success") {
|
|
302
|
+
console.error(
|
|
303
|
+
`\n[arbiter] call failed: status=${(response as any).status} ` +
|
|
304
|
+
`error=${(response as any).error ?? "(none)"}`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
console.log("\n" + response.text);
|
|
308
|
+
} finally {
|
|
309
|
+
rl.close();
|
|
310
|
+
await closeBridge();
|
|
311
|
+
if (restoreGeminiSettings) restoreGeminiSettings();
|
|
312
|
+
const cs = engine.getConstraintState();
|
|
313
|
+
console.log(
|
|
314
|
+
`\nSession complete. Rounds: ${cs.rounds_completed}, ` +
|
|
315
|
+
`Cost: $${cs.budget_spent.toFixed(4)}, ` +
|
|
316
|
+
`Time: ${cs.elapsed_minutes.toFixed(1)}min`,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createServer, Socket } from "node:net";
|
|
2
|
+
import { unlinkSync, existsSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
export interface BridgeHandlers {
|
|
5
|
+
delegate: (params: { to: string | string[]; message: string }) => Promise<unknown>;
|
|
6
|
+
end: (params: { closing_message: string }) => Promise<unknown>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function startBridgeServer(
|
|
10
|
+
socketPath: string,
|
|
11
|
+
handlers: BridgeHandlers,
|
|
12
|
+
): Promise<() => Promise<void>> {
|
|
13
|
+
if (existsSync(socketPath)) unlinkSync(socketPath);
|
|
14
|
+
|
|
15
|
+
const open = new Set<Socket>();
|
|
16
|
+
|
|
17
|
+
const server = createServer((sock: Socket) => {
|
|
18
|
+
open.add(sock);
|
|
19
|
+
sock.on("close", () => open.delete(sock));
|
|
20
|
+
let buf = "";
|
|
21
|
+
sock.on("data", async (chunk) => {
|
|
22
|
+
buf += chunk.toString("utf-8");
|
|
23
|
+
let nl: number;
|
|
24
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
25
|
+
const line = buf.slice(0, nl);
|
|
26
|
+
buf = buf.slice(nl + 1);
|
|
27
|
+
if (!line.trim()) continue;
|
|
28
|
+
let req: any;
|
|
29
|
+
try { req = JSON.parse(line); } catch { continue; }
|
|
30
|
+
try {
|
|
31
|
+
let result: unknown;
|
|
32
|
+
if (req.method === "delegate") result = await handlers.delegate(req.params);
|
|
33
|
+
else if (req.method === "end") result = await handlers.end(req.params);
|
|
34
|
+
else throw new Error(`unknown method: ${req.method}`);
|
|
35
|
+
sock.write(JSON.stringify({ id: req.id, result }) + "\n");
|
|
36
|
+
} catch (err: any) {
|
|
37
|
+
sock.write(JSON.stringify({ id: req.id, error: String(err?.message ?? err) }) + "\n");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
sock.on("error", () => { /* client disconnect is fine */ });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await new Promise<void>((resolve, reject) => {
|
|
45
|
+
server.once("error", reject);
|
|
46
|
+
server.listen(socketPath, () => resolve());
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return async () => {
|
|
50
|
+
for (const s of open) s.destroy();
|
|
51
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
52
|
+
if (existsSync(socketPath)) unlinkSync(socketPath);
|
|
53
|
+
};
|
|
54
|
+
}
|
package/src/commands/run.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { join, resolve, basename } from "node:path";
|
|
|
7
7
|
import { c, type ParsedArgs } from "../colors";
|
|
8
8
|
import { getHarnessRoot, discoverDirs, promptSelect, getAdapterDir } from "../utils";
|
|
9
9
|
import type { TranscriptEntry } from "@aos-harness/runtime/types";
|
|
10
|
+
import { runAdapterSession } from "../adapter-session";
|
|
11
|
+
import { readAdapterConfig } from "../adapter-config";
|
|
10
12
|
|
|
11
13
|
function createEventBuffer(platformUrl: string, sessionId: string) {
|
|
12
14
|
const buffer: TranscriptEntry[] = [];
|
|
@@ -304,7 +306,7 @@ ${c.bold(`AOS ${sessionType} Session`)}
|
|
|
304
306
|
Output: ${c.cyan(deliberationDir)}
|
|
305
307
|
`);
|
|
306
308
|
|
|
307
|
-
//
|
|
309
|
+
// Determine adapter: --adapter flag > .aos/config.yaml > default "pi"
|
|
308
310
|
let platformUrl = (args.flags["platform-url"] as string) || null;
|
|
309
311
|
const aosConfigPath = join(process.cwd(), ".aos", "config.yaml");
|
|
310
312
|
let adapter = "pi";
|
|
@@ -317,6 +319,7 @@ ${c.bold(`AOS ${sessionType} Session`)}
|
|
|
317
319
|
platformUrl = (config.platform as Record<string, unknown>).url as string;
|
|
318
320
|
}
|
|
319
321
|
}
|
|
322
|
+
if (args.flags["adapter"]) adapter = args.flags["adapter"] as string;
|
|
320
323
|
|
|
321
324
|
const adapterName = adapter === "claude-code" ? "claude-code" : adapter;
|
|
322
325
|
// Resolve adapter from: 1) project dir, 2) installed package, 3) monorepo
|
|
@@ -374,13 +377,19 @@ ${c.bold(`AOS ${sessionType} Session`)}
|
|
|
374
377
|
}
|
|
375
378
|
process.exit(exitCode);
|
|
376
379
|
} else {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
380
|
+
const adapterConfig = readAdapterConfig(root);
|
|
381
|
+
await runAdapterSession({
|
|
382
|
+
platform: adapter,
|
|
383
|
+
profileDir: profileDir!,
|
|
384
|
+
briefPath,
|
|
385
|
+
domainName,
|
|
386
|
+
root,
|
|
387
|
+
sessionId,
|
|
388
|
+
deliberationDir,
|
|
389
|
+
verbose: !!args.flags.verbose,
|
|
390
|
+
workflowConfig: isExecutionProfile ? workflowConfig : null,
|
|
391
|
+
workflowsDir,
|
|
392
|
+
modelOverrides: adapterConfig?.model_overrides,
|
|
393
|
+
});
|
|
385
394
|
}
|
|
386
395
|
}
|
package/src/gauges.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const BAR_WIDTH = 16;
|
|
2
|
+
|
|
3
|
+
function color(text: string, code: string): string {
|
|
4
|
+
return `\x1b[${code}m${text}\x1b[0m`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function renderTextGauge(
|
|
8
|
+
label: string,
|
|
9
|
+
value: number,
|
|
10
|
+
min: number,
|
|
11
|
+
max: number,
|
|
12
|
+
unit: string,
|
|
13
|
+
): string {
|
|
14
|
+
const ratio = Math.max(0, Math.min(1, value / max));
|
|
15
|
+
const filled = Math.round(ratio * BAR_WIDTH);
|
|
16
|
+
const bar = "█".repeat(filled) + "░".repeat(BAR_WIDTH - filled);
|
|
17
|
+
const colored = value < min ? color(bar, "32") : value >= max * 0.9 ? color(bar, "31") : color(bar, "33");
|
|
18
|
+
const valueStr = unit === "$" ? `$${value.toFixed(2)}` : `${value.toFixed(1)} ${unit}`;
|
|
19
|
+
const minStr = unit === "$" ? `$${min}` : `${min}`;
|
|
20
|
+
const maxStr = unit === "$" ? `$${max}` : `${max}`;
|
|
21
|
+
return ` ${label.padEnd(7)} ${valueStr.padEnd(10)} [${colored}] (min: ${minStr}, max: ${maxStr})`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RoundSummary {
|
|
25
|
+
round: number;
|
|
26
|
+
maxRounds: number;
|
|
27
|
+
minutes: number;
|
|
28
|
+
dollars: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function renderRoundOneLiner(s: RoundSummary): string {
|
|
32
|
+
return color(`[Round ${s.round}/${s.maxRounds} · ${s.minutes.toFixed(1)}min · $${s.dollars.toFixed(2)}]`, "90");
|
|
33
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import {
|
|
5
|
+
CallToolRequestSchema,
|
|
6
|
+
ListToolsRequestSchema,
|
|
7
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import { connect, Socket } from "node:net";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
|
|
11
|
+
const SOCK = process.env.AOS_BRIDGE_SOCKET;
|
|
12
|
+
if (!SOCK) {
|
|
13
|
+
console.error("AOS_BRIDGE_SOCKET env var is required");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let sock: Socket | null = null;
|
|
18
|
+
const pending = new Map<string, (msg: any) => void>();
|
|
19
|
+
let buf = "";
|
|
20
|
+
|
|
21
|
+
function ensureSock(): Promise<Socket> {
|
|
22
|
+
if (sock && !sock.destroyed) return Promise.resolve(sock);
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const s = connect(SOCK!);
|
|
25
|
+
s.on("connect", () => { sock = s; resolve(s); });
|
|
26
|
+
s.on("error", reject);
|
|
27
|
+
s.on("data", (chunk) => {
|
|
28
|
+
buf += chunk.toString("utf-8");
|
|
29
|
+
let nl: number;
|
|
30
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
31
|
+
const line = buf.slice(0, nl);
|
|
32
|
+
buf = buf.slice(nl + 1);
|
|
33
|
+
if (!line.trim()) continue;
|
|
34
|
+
try {
|
|
35
|
+
const msg = JSON.parse(line);
|
|
36
|
+
const cb = pending.get(msg.id);
|
|
37
|
+
if (cb) { pending.delete(msg.id); cb(msg); }
|
|
38
|
+
} catch { /* ignore */ }
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
s.on("close", () => { sock = null; });
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function rpc(method: string, params: unknown): Promise<unknown> {
|
|
46
|
+
const s = await ensureSock();
|
|
47
|
+
const id = randomUUID();
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
pending.set(id, (msg) => {
|
|
50
|
+
if (msg.error) reject(new Error(msg.error));
|
|
51
|
+
else resolve(msg.result);
|
|
52
|
+
});
|
|
53
|
+
s.write(JSON.stringify({ id, method, params }) + "\n");
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const server = new Server(
|
|
58
|
+
{ name: "aos-arbiter-bridge", version: "0.1.0" },
|
|
59
|
+
{ capabilities: { tools: {} } },
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
63
|
+
tools: [
|
|
64
|
+
{
|
|
65
|
+
name: "delegate",
|
|
66
|
+
description: "Delegate a message to one or more participant agents and receive their responses.",
|
|
67
|
+
inputSchema: {
|
|
68
|
+
type: "object" as const,
|
|
69
|
+
properties: {
|
|
70
|
+
to: { description: "Agent id, list of ids, or 'all'", type: "string" },
|
|
71
|
+
message: { type: "string" },
|
|
72
|
+
},
|
|
73
|
+
required: ["to", "message"],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "end",
|
|
78
|
+
description: "End the deliberation. Provide a closing summary message.",
|
|
79
|
+
inputSchema: {
|
|
80
|
+
type: "object" as const,
|
|
81
|
+
properties: { closing_message: { type: "string" } },
|
|
82
|
+
required: ["closing_message"],
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
89
|
+
const result = await rpc(req.params.name, req.params.arguments ?? {});
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await server.connect(new StdioServerTransport());
|