babavoss 0.0.1 → 0.0.2
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/NOTICE +7 -0
- package/bin/voss.ts +48 -0
- package/gui/babavoss-web.js +234 -0
- package/gui/chunk-5hpp1ypv.js +11710 -0
- package/gui/chunk-9rq662fd.js +8627 -0
- package/gui/chunk-k3cm8j6r.js +433 -0
- package/gui/chunk-pk09y98y.js +50 -0
- package/gui/chunk-smz02qa6.js +186 -0
- package/gui/chunk-wwqypxre.js +49 -0
- package/gui/gui.js +2015 -0
- package/gui/react-compiler-runtime.js +39 -0
- package/gui/react-dom-client.js +21 -0
- package/gui/react-dom.js +44 -0
- package/gui/react-jsx-runtime.js +19 -0
- package/gui/react.js +105 -0
- package/gui/theme.css +3053 -0
- package/index.ts +15 -0
- package/package.json +50 -4
- package/src/baba/check.ts +90 -0
- package/src/baba/config.ts +261 -0
- package/src/baba/find.ts +12 -0
- package/src/baba/init.ts +176 -0
- package/src/baba/node.ts +483 -0
- package/src/baba/project.ts +63 -0
- package/src/baba/registry.ts +35 -0
- package/src/baba/worker.ts +55 -0
- package/src/bench/index.ts +6 -0
- package/src/bench/measure.ts +132 -0
- package/src/bench/scenarios.ts +136 -0
- package/src/build/builder.ts +74 -0
- package/src/build/failure.ts +78 -0
- package/src/build/guard.ts +85 -0
- package/src/build/mdx-register.ts +3 -0
- package/src/build/mdx.ts +38 -0
- package/src/build/project.ts +38 -0
- package/src/build/views.ts +146 -0
- package/src/builder/main.ts +29 -0
- package/src/desktop/bob.ts +76 -0
- package/src/desktop/desktop.css +111 -0
- package/src/desktop/icons.ts +50 -0
- package/src/desktop/index.ts +323 -0
- package/src/desktop/routes.ts +98 -0
- package/src/desktop/view.tsx +673 -0
- package/src/door/core.ts +384 -0
- package/src/ecs/baba.ts +431 -0
- package/src/ecs/codec.ts +334 -0
- package/src/ecs/handles.ts +91 -0
- package/src/ecs/replica.ts +150 -0
- package/src/ecs/runtime.ts +603 -0
- package/src/ecs/scheduler.ts +75 -0
- package/src/ecs/snapshot.ts +102 -0
- package/src/ecs/state.ts +759 -0
- package/src/ecs/system.ts +256 -0
- package/src/ecs/table.ts +420 -0
- package/src/ecs/testbed.ts +97 -0
- package/src/exec/host.ts +177 -0
- package/src/exec/main.ts +98 -0
- package/src/exec/watch.ts +7 -0
- package/src/exec/wire.ts +29 -0
- package/src/generated/build.ts +4 -0
- package/src/gui/css.d.ts +1 -0
- package/src/gui/gui.tsx +245 -0
- package/src/gui/index.ts +51 -0
- package/src/gui/inspector.tsx +47 -0
- package/src/gui/levels.tsx +73 -0
- package/src/gui/promptware.tsx +68 -0
- package/src/gui/runner.tsx +118 -0
- package/src/gui/theme.css +498 -0
- package/src/gui/theme.ts +25 -0
- package/src/gui/wizard.tsx +227 -0
- package/src/guide/add-a-desktop.mdx +100 -0
- package/src/guide/compose-an-interface.mdx +84 -0
- package/src/guide/index.ts +13 -0
- package/src/guide/reach-outside.mdx +112 -0
- package/src/guide/spec-a-system.mdx +93 -0
- package/src/guide/systems-together.mdx +72 -0
- package/src/guide/write-a-system.mdx +183 -0
- package/src/guide/write-promptware.mdx +90 -0
- package/src/http/server.ts +310 -0
- package/src/kernel/build.ts +21 -0
- package/src/kernel/builder.ts +105 -0
- package/src/kernel/children.ts +117 -0
- package/src/kernel/context.ts +90 -0
- package/src/kernel/lock.ts +46 -0
- package/src/kernel/names.ts +14 -0
- package/src/kernel/schema.ts +130 -0
- package/src/kernel/where.ts +12 -0
- package/src/kit/index.ts +232 -0
- package/src/maker/system.ts +213 -0
- package/src/mcp/daemon.ts +61 -0
- package/src/mcp/main.ts +208 -0
- package/src/mcp/rpc.ts +64 -0
- package/src/mcp/tools.ts +125 -0
- package/src/prompt/evals.ts +42 -0
- package/src/prompt/index.ts +242 -0
- package/src/prompt/jsx-dev-runtime.ts +1 -0
- package/src/prompt/jsx-runtime.ts +49 -0
- package/src/prompt/mdx.d.ts +1 -0
- package/src/promptware/compile.ts +183 -0
- package/src/promptware/define.ts +16 -0
- package/src/promptware/disk.ts +72 -0
- package/src/promptware/markdown.d.ts +6 -0
- package/src/promptware/sync.ts +437 -0
- package/src/promptware/system.ts +215 -0
- package/src/runtime/bridge.ts +85 -0
- package/src/runtime/connect.ts +54 -0
- package/src/runtime/env.ts +35 -0
- package/src/runtime/harness.ts +80 -0
- package/src/runtime/main.ts +119 -0
- package/src/runtime/worker.ts +33 -0
- package/src/server/edge.ts +332 -0
- package/src/server/main.ts +45 -0
- package/src/server/messages.ts +97 -0
- package/src/server/protocol.ts +37 -0
- package/src/services/args.ts +45 -0
- package/src/services/exec.ts +69 -0
- package/src/services/fs.ts +139 -0
- package/src/services/http.ts +30 -0
- package/src/services/index.ts +113 -0
- package/src/services/secrets.ts +18 -0
- package/src/shell/address.ts +21 -0
- package/src/shell/args.ts +219 -0
- package/src/shell/client.ts +107 -0
- package/src/shell/codes.ts +26 -0
- package/src/shell/positional.ts +20 -0
- package/src/shell/run.ts +470 -0
- package/src/shell/service.ts +167 -0
- package/src/shell/state.ts +204 -0
- package/src/spec/adapters.ts +72 -0
- package/src/spec/diff.ts +26 -0
- package/src/spec/files.ts +17 -0
- package/src/spec/index.ts +155 -0
- package/src/spec/run.ts +97 -0
- package/src/spec/take.ts +54 -0
- package/src/test/index.ts +8 -0
- package/src/test/prove.ts +56 -0
- package/src/test/records.ts +23 -0
- package/src/test/specs.ts +56 -0
- package/src/test/steps.ts +100 -0
- package/src/test/voss-dir.ts +17 -0
- package/src/transport/messages.ts +110 -0
- package/src/transport/transport.ts +62 -0
- package/src/wall/probe.ts +67 -0
- package/src/wall/profile.ts +103 -0
- package/src/wall/spawn.ts +59 -0
- package/src/web/app.tsx +53 -0
- package/src/web/core.tsx +140 -0
- package/src/web/form.ts +155 -0
- package/src/web/hooks.ts +135 -0
- package/src/web/index.tsx +17 -0
- package/src/web/list.ts +19 -0
- package/src/web/maker.tsx +766 -0
- package/src/web/objects.tsx +213 -0
- package/src/web/socket.ts +84 -0
- package/src/web/state.tsx +69 -0
- package/src/web/store.ts +221 -0
- package/src/web/ui.tsx +135 -0
- package/README.md +0 -5
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// The MCP server's way to the running voss: where it listens, its token, and
|
|
2
|
+
// the API calls the tools need. The server is a client of the daemon only; it
|
|
3
|
+
// never starts a baba of its own.
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { realpath } from "node:fs/promises";
|
|
6
|
+
import type { Project } from "../baba/project.ts";
|
|
7
|
+
import type { Described } from "../baba/node.ts";
|
|
8
|
+
import type { StateAnswer } from "../ecs/snapshot.ts";
|
|
9
|
+
import { daemon as shellDaemon } from "../shell/client.ts";
|
|
10
|
+
|
|
11
|
+
export class ApiError extends Error {
|
|
12
|
+
constructor(readonly code: string, msg: string) { super(msg); }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Daemon {
|
|
16
|
+
base: string;
|
|
17
|
+
describe(since?: number, signal?: AbortSignal): Promise<Described>;
|
|
18
|
+
fire(name: string, args: unknown, key?: string): Promise<unknown>;
|
|
19
|
+
read(name: string, args: unknown): Promise<unknown>;
|
|
20
|
+
state(since?: number, signal?: AbortSignal): Promise<StateAnswer>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The daemon's base URL: $VOSS_URL, else the way the shell finds it, through ROOT/address. */
|
|
24
|
+
export async function findBase(rootDir: string): Promise<string | null> {
|
|
25
|
+
if (process.env.VOSS_URL) return process.env.VOSS_URL.replace(/\/+$/, "");
|
|
26
|
+
return (await shellDaemon(rootDir))?.base ?? null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const same = async (a: string, b: string) => a === b || (await realpath(a).catch(() => a)) === (await realpath(b).catch(() => b));
|
|
30
|
+
|
|
31
|
+
/** The daemon's state for `project`, or why there is none. */
|
|
32
|
+
export async function daemonFor(project: Project, rootDir: string): Promise<Daemon> {
|
|
33
|
+
const base = await findBase(rootDir);
|
|
34
|
+
if (!base) throw new ApiError("down", "no running voss found: start `voss gui` or `voss service install`, or set VOSS_URL");
|
|
35
|
+
const token = process.env.VOSS_TOKEN ?? (await Bun.file(join(rootDir, "http-token")).text().catch(() => "")).trim();
|
|
36
|
+
if (!token) throw new ApiError("unauthorized", `no token in ${join(rootDir, "http-token")}`);
|
|
37
|
+
const headers = { authorization: `Bearer ${token}`, "content-type": "application/json" };
|
|
38
|
+
const name = encodeURIComponent(project.name);
|
|
39
|
+
|
|
40
|
+
const answer = async (r: Response) => {
|
|
41
|
+
const j = await r.json().catch(() => ({})) as { value?: unknown; error?: { code: string; message: string } };
|
|
42
|
+
if (!r.ok || j.error) throw new ApiError(j.error?.code ?? "failed", j.error?.message ?? r.statusText);
|
|
43
|
+
return j;
|
|
44
|
+
};
|
|
45
|
+
const post = async (path: string, body: unknown, signal?: AbortSignal) =>
|
|
46
|
+
(await answer(await fetch(`${base}/api/${path}`, { method: "POST", headers, body: JSON.stringify(body ?? {}), signal }))).value;
|
|
47
|
+
|
|
48
|
+
let known: { projects?: { name: string; dir: string }[] };
|
|
49
|
+
try { known = await answer(await fetch(`${base}/api/projects`, { headers, signal: AbortSignal.timeout(3000) })) as typeof known; }
|
|
50
|
+
catch (err) { throw err instanceof ApiError ? err : new ApiError("down", `voss at ${base} does not answer: ${err instanceof Error ? err.message : String(err)}`); }
|
|
51
|
+
const entry = known.projects?.find((p) => p.name === project.name);
|
|
52
|
+
if (!entry || !(await same(entry.dir, project.dir))) throw new ApiError("noproject", `voss at ${base} does not run ${project.name} from ${project.dir}`);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
base,
|
|
56
|
+
describe: async (since = 0, signal) => await answer(await fetch(`${base}/api/describe/${name}${since ? `?since=${since}` : ""}`, { headers, signal })) as unknown as Described,
|
|
57
|
+
fire: (n, args, key) => post(`fire/${name}/${encodeURIComponent(n)}${key ? `?key=${encodeURIComponent(key)}` : ""}`, args),
|
|
58
|
+
read: (n, args) => post(`read/${name}/${encodeURIComponent(n)}`, args),
|
|
59
|
+
state: (since = 0, signal) => post(`state/${name}`, { since }, signal) as Promise<StateAnswer>,
|
|
60
|
+
};
|
|
61
|
+
}
|
package/src/mcp/main.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// `voss baba mcp`: the baba as an MCP server over stdio, a client of the
|
|
2
|
+
// running voss. By default six fixed tools, the CLI's operations over the one
|
|
3
|
+
// core (door/core.ts): `manifest` teaches the contract, `call` and `read` use
|
|
4
|
+
// it, `follow`, `watch` and `state` read the state. With VOSS_MCP_TOOLS=1,
|
|
5
|
+
// one tool per action and query too. Resources are the baba's context and the
|
|
6
|
+
// answers over the budget; prompts are the skills. A reload in the daemon
|
|
7
|
+
// refreshes them and tells the client.
|
|
8
|
+
import { join, basename } from "node:path";
|
|
9
|
+
import { readFile } from "node:fs/promises";
|
|
10
|
+
import type { Project } from "../baba/project.ts";
|
|
11
|
+
import { defaultHarness, type Manifest } from "../ecs/baba.ts";
|
|
12
|
+
import { readManifest } from "../baba/node.ts";
|
|
13
|
+
import { serveRpc, RpcError, NO_METHOD, PARAMS } from "./rpc.ts";
|
|
14
|
+
import { daemonFor, type Daemon } from "./daemon.ts";
|
|
15
|
+
import { follow as whole } from "../ecs/snapshot.ts";
|
|
16
|
+
import { toolsOf, callName, promptsOf, promptText, type Tools } from "./tools.ts";
|
|
17
|
+
import { BUDGET, DoorError, budget, errorOf, index, schemaOf, followEntity, watchQuery, stateOf, untilOf, byKey, isTruncated, type Outcome } from "../door/core.ts";
|
|
18
|
+
|
|
19
|
+
const VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
|
|
20
|
+
const empty: Manifest = { systems: [], promptware: { context: null, skills: {}, hooks: [] }, harness: defaultHarness, components: {}, resources: {}, effects: {}, actions: {}, queries: {}, tick: null };
|
|
21
|
+
|
|
22
|
+
const log = (l: string) => process.stderr.write(`voss mcp: ${l}\n`);
|
|
23
|
+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
24
|
+
type O = Record<string, unknown>;
|
|
25
|
+
|
|
26
|
+
export async function runMcp(project: Project, rootDir: string): Promise<number> {
|
|
27
|
+
let daemon: Daemon;
|
|
28
|
+
try { daemon = await daemonFor(project, rootDir); }
|
|
29
|
+
catch (err) { log(err instanceof Error ? err.message : String(err)); return 1; }
|
|
30
|
+
const version = ((await import("../../package.json")) as { version: string }).version;
|
|
31
|
+
const perCall = process.env.VOSS_MCP_TOOLS === "1";
|
|
32
|
+
|
|
33
|
+
let gen = 0;
|
|
34
|
+
let m: Manifest = empty;
|
|
35
|
+
let down: string | null = null;
|
|
36
|
+
let loadError: string | null = null;
|
|
37
|
+
let tools: Tools = toolsOf(m, { perCall });
|
|
38
|
+
// Down, the contract is the one the last load wrote, as the CLI's is.
|
|
39
|
+
const take = async (d: { gen: number; manifest: Manifest | null; error: string | null }) => {
|
|
40
|
+
gen = d.gen;
|
|
41
|
+
m = d.manifest ?? (await readManifest(project)) ?? empty;
|
|
42
|
+
loadError = d.error;
|
|
43
|
+
down = d.manifest ? null : `${project.name} is not running${d.error ? `: ${d.error}` : ""}`;
|
|
44
|
+
tools = toolsOf(m, { perCall });
|
|
45
|
+
};
|
|
46
|
+
await take(await daemon.describe());
|
|
47
|
+
if (down) log(down);
|
|
48
|
+
|
|
49
|
+
// Answers over the budget, kept for this session as resources.
|
|
50
|
+
const outDir = join(project.baba, ".voss", "out");
|
|
51
|
+
const outs = new Map<string, string>();
|
|
52
|
+
|
|
53
|
+
const peer = serveRpc(Bun.stdin.stream(), (l) => { process.stdout.write(l); }, handle);
|
|
54
|
+
|
|
55
|
+
/** A marker of an answer over the budget, kept as a resource. */
|
|
56
|
+
const keep = (t: { path: string }): string => {
|
|
57
|
+
const file = basename(t.path);
|
|
58
|
+
outs.set(file, t.path);
|
|
59
|
+
peer.notify("notifications/resources/list_changed");
|
|
60
|
+
return `baba://out/${file}`;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** A value as a tool's answer, within the budget; `isError` for a wait that did not hold. */
|
|
64
|
+
function answer(tool: string, value: unknown, structured: boolean, isError = false): O {
|
|
65
|
+
if (Array.isArray(value) && isTruncated(value.at(-1))) {
|
|
66
|
+
const t = value.at(-1) as { path: string };
|
|
67
|
+
value = [...value.slice(0, -1), { ...t, resource: keep(t) }];
|
|
68
|
+
}
|
|
69
|
+
const v = budget(value, { limit: BUDGET, dir: outDir, name: tool });
|
|
70
|
+
if (isTruncated(v)) return { content: [{ type: "text", text: JSON.stringify({ ...v, resource: keep(v) }) }], ...(isError ? { isError } : {}) };
|
|
71
|
+
return {
|
|
72
|
+
content: [{ type: "text", text: JSON.stringify(v ?? null) }],
|
|
73
|
+
...(structured && typeof v === "object" && v !== null && !Array.isArray(v) ? { structuredContent: v } : {}),
|
|
74
|
+
...(isError ? { isError } : {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const waited = (tool: string, r: Outcome) => answer(tool, r.value, true, r.outcome !== "held");
|
|
78
|
+
const failed = (err: unknown): O => ({ content: [{ type: "text", text: JSON.stringify({ error: errorOf(err) }) }], isError: true });
|
|
79
|
+
|
|
80
|
+
const secs = (a: O) => typeof a.for === "number" && a.for >= 0 ? 1000 * Math.min(a.for, 600) : undefined;
|
|
81
|
+
const str = (v: unknown) => typeof v === "string" && v ? v : undefined;
|
|
82
|
+
const live = () => { if (down) throw new DoorError("down", down); };
|
|
83
|
+
|
|
84
|
+
async function call(name: string, a: O): Promise<O> {
|
|
85
|
+
try {
|
|
86
|
+
if (name === "manifest") {
|
|
87
|
+
const n = str(a.name);
|
|
88
|
+
if (n) {
|
|
89
|
+
const c = callName(m, n);
|
|
90
|
+
const v = c && schemaOf(m, c.name);
|
|
91
|
+
if (!v) throw new DoorError("noaction", `no action or query ${n}`);
|
|
92
|
+
return answer(name, v, true);
|
|
93
|
+
}
|
|
94
|
+
const v = index(m, { via: down ? "down" : daemon.base, gen, error: loadError });
|
|
95
|
+
const system = str(a.system);
|
|
96
|
+
return answer(name, system ? { ...v, calls: v.calls.filter((c) => c.system === system) } : v, true);
|
|
97
|
+
}
|
|
98
|
+
live();
|
|
99
|
+
if (name === "follow") {
|
|
100
|
+
let e = a.entity;
|
|
101
|
+
if (e === undefined && str(a.component) && (typeof a.key === "string" || typeof a.key === "number")) e = byKey(whole(null, await daemon.state(-1)), m, a.component as string, a.key);
|
|
102
|
+
if (typeof e !== "number" || !Number.isInteger(e)) throw new DoorError("args", "follow takes an entity, a whole number, or a component and its key");
|
|
103
|
+
const until = str(a.until) ? untilOf(m, a.until as string) : null;
|
|
104
|
+
return waited(name, await followEntity(daemon, e, { until, for: secs(a) }));
|
|
105
|
+
}
|
|
106
|
+
if (name === "watch") {
|
|
107
|
+
const n = str(a.name) ?? str(a.query);
|
|
108
|
+
const q = n ? callName(m, n) : null;
|
|
109
|
+
if (!q || q.kind !== "query") throw new DoorError("noquery", `no query ${String(n)}; queries: ${Object.keys(m.queries).join(", ")}`);
|
|
110
|
+
return waited(name, await watchQuery(daemon, q.name, a.args ?? {}, { for: secs(a) }));
|
|
111
|
+
}
|
|
112
|
+
if (name === "state") {
|
|
113
|
+
const r = await stateOf(daemon, m, {
|
|
114
|
+
component: str(a.component),
|
|
115
|
+
entity: typeof a.entity === "number" ? a.entity : undefined,
|
|
116
|
+
key: typeof a.key === "string" || typeof a.key === "number" ? a.key : undefined,
|
|
117
|
+
where: typeof a.where === "object" && a.where !== null ? a.where as O : undefined,
|
|
118
|
+
limit: typeof a.limit === "number" && a.limit > 0 ? a.limit : undefined,
|
|
119
|
+
all: a.all === true, deep: a.deep === true, dir: outDir,
|
|
120
|
+
});
|
|
121
|
+
return waited(name, r);
|
|
122
|
+
}
|
|
123
|
+
if (name === "call" || name === "read") {
|
|
124
|
+
const n = String(a.name ?? "");
|
|
125
|
+
const c = callName(m, n);
|
|
126
|
+
if (!c) throw new DoorError(name === "read" ? "noquery" : "noaction", `no ${name === "read" ? "query" : "action or query"} ${n}; \`manifest\` lists them`);
|
|
127
|
+
if (name === "read" && c.kind !== "query") throw new DoorError("noquery", `${n} is an action; \`call\` fires it`);
|
|
128
|
+
const args = a.args ?? {};
|
|
129
|
+
const v = c.kind === "query" ? await daemon.read(c.name, args) : await daemon.fire(c.name, args, str(a.key));
|
|
130
|
+
return answer(name, v, false);
|
|
131
|
+
}
|
|
132
|
+
const t = tools.targets.get(name);
|
|
133
|
+
if (!t) throw new DoorError("noaction", `no tool ${name}; tools/list names them`);
|
|
134
|
+
// `_key` on an action's tool is the idempotency key, not an arg.
|
|
135
|
+
const { _key, ...rest } = a as { _key?: unknown } & O;
|
|
136
|
+
const v = t.kind === "action" ? await daemon.fire(t.name, rest, typeof _key === "string" ? _key : undefined) : await daemon.read(t.name, rest);
|
|
137
|
+
return answer(name, v, true);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
return failed(err);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function handle(method: string, p: O): Promise<unknown> {
|
|
144
|
+
switch (method) {
|
|
145
|
+
case "initialize": {
|
|
146
|
+
const asked = typeof p.protocolVersion === "string" && VERSIONS.includes(p.protocolVersion) ? p.protocolVersion : VERSIONS[0];
|
|
147
|
+
return {
|
|
148
|
+
protocolVersion: asked,
|
|
149
|
+
capabilities: { tools: { listChanged: perCall }, resources: { listChanged: true }, prompts: {} },
|
|
150
|
+
serverInfo: { name: project.name, version },
|
|
151
|
+
instructions: `${project.title}: the baba of ${project.dir}. \`manifest\` lists every call with an example; \`manifest\` with a name gives its schemas. \`call\` fires an action or reads a query by name, \`read\` reads a query; an action that starts work answers an entity to give \`follow\`. \`watch\` waits for a query's answer to change, \`state\` reads the state.`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
case "ping": return {};
|
|
155
|
+
case "tools/list": return { tools: tools.tools };
|
|
156
|
+
case "tools/call": {
|
|
157
|
+
if (typeof p.name !== "string") throw new RpcError(PARAMS, "tools/call takes a name");
|
|
158
|
+
return call(p.name, (typeof p.arguments === "object" && p.arguments !== null ? p.arguments : {}) as O);
|
|
159
|
+
}
|
|
160
|
+
case "resources/list": return {
|
|
161
|
+
resources: [
|
|
162
|
+
...(m.promptware.context ? [{ uri: "baba://context", name: "context", description: "what the baba tells agents", mimeType: "text/markdown" }] : []),
|
|
163
|
+
...[...outs.keys()].map((f) => ({ uri: `baba://out/${f}`, name: f, description: "an answer over the output budget", mimeType: "application/json" })),
|
|
164
|
+
],
|
|
165
|
+
};
|
|
166
|
+
case "resources/read": {
|
|
167
|
+
const uri = String(p.uri ?? "");
|
|
168
|
+
const context = uri === "baba://context" ? m.promptware.context : null;
|
|
169
|
+
if (context) return { contents: [{ uri, mimeType: "text/markdown", text: context }] };
|
|
170
|
+
const out = uri.startsWith("baba://out/") ? outs.get(basename(uri.slice(11))) : undefined;
|
|
171
|
+
if (out) return { contents: [{ uri, mimeType: "application/json", text: await readFile(out, "utf8") }] };
|
|
172
|
+
throw new RpcError(-32002, `no resource ${uri}`, { uri });
|
|
173
|
+
}
|
|
174
|
+
case "resources/templates/list": return { resourceTemplates: [] };
|
|
175
|
+
case "prompts/list": return { prompts: promptsOf(m).map(({ name, description }) => ({ name, description })) };
|
|
176
|
+
case "prompts/get": {
|
|
177
|
+
const pr = promptsOf(m).find((x) => x.name === p.name);
|
|
178
|
+
const text = pr && promptText(m, pr.skill, tools.generic);
|
|
179
|
+
if (!pr || !text) throw new RpcError(PARAMS, `no prompt ${String(p.name)}`);
|
|
180
|
+
return { description: pr.description, messages: [{ role: "user", content: { type: "text", text } }] };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
throw new RpcError(NO_METHOD, `no method ${method}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Reload: the daemon answers describe past a generation when it moves, or after a while as it is.
|
|
187
|
+
const stop = new AbortController();
|
|
188
|
+
void (async () => {
|
|
189
|
+
while (!stop.signal.aborted) {
|
|
190
|
+
try {
|
|
191
|
+
const asked = Date.now();
|
|
192
|
+
const d = await daemon.describe(gen, stop.signal);
|
|
193
|
+
if (d.gen === gen) { if (Date.now() - asked < 1000) await sleep(1000); continue; }
|
|
194
|
+
await take(d);
|
|
195
|
+
log(`generation ${gen}${down ? `: ${down}` : ""}`);
|
|
196
|
+
// The six never change; only the per-call list does.
|
|
197
|
+
if (perCall) peer.notify("notifications/tools/list_changed");
|
|
198
|
+
peer.notify("notifications/resources/list_changed");
|
|
199
|
+
} catch {
|
|
200
|
+
if (!stop.signal.aborted) await sleep(2000);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
})();
|
|
204
|
+
|
|
205
|
+
await peer.done;
|
|
206
|
+
stop.abort();
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
package/src/mcp/rpc.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// JSON-RPC 2.0 as stdio MCP speaks it: one JSON object per line, both ways.
|
|
2
|
+
// A request gets one answer, in any order; a notification gets none. The
|
|
3
|
+
// handler throws an RpcError for a protocol error; anything else it throws
|
|
4
|
+
// is an internal error.
|
|
5
|
+
|
|
6
|
+
export class RpcError extends Error {
|
|
7
|
+
constructor(readonly code: number, msg: string, readonly data?: unknown) { super(msg); }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const PARSE = -32700, INVALID = -32600, NO_METHOD = -32601, PARAMS = -32602, INTERNAL = -32603;
|
|
11
|
+
|
|
12
|
+
type Id = string | number | null;
|
|
13
|
+
export type Handler = (method: string, params: Record<string, unknown>) => Promise<unknown>;
|
|
14
|
+
|
|
15
|
+
export interface Peer {
|
|
16
|
+
/** A notification to the client. */
|
|
17
|
+
notify(method: string, params?: Record<string, unknown>): void;
|
|
18
|
+
/** Resolves when the input closes and every request is answered. */
|
|
19
|
+
done: Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Reads lines from `input`, answers each through `write`, one line each. */
|
|
23
|
+
export function serveRpc(input: ReadableStream<Uint8Array>, write: (line: string) => void, handle: Handler, notified: (method: string, params: Record<string, unknown>) => void = () => {}): Peer {
|
|
24
|
+
const send = (o: unknown) => write(JSON.stringify(o) + "\n");
|
|
25
|
+
const error = (id: Id, code: number, message: string, data?: unknown) => send({ jsonrpc: "2.0", id, error: { code, message, ...(data === undefined ? {} : { data }) } });
|
|
26
|
+
const pending = new Set<Promise<void>>();
|
|
27
|
+
|
|
28
|
+
function one(msg: unknown): void {
|
|
29
|
+
if (typeof msg !== "object" || msg === null || Array.isArray(msg)) { error(null, INVALID, "not a request"); return; }
|
|
30
|
+
const m = msg as { jsonrpc?: unknown; id?: Id; method?: unknown; params?: unknown };
|
|
31
|
+
if (m.method === undefined && ("result" in m || "error" in m)) return; // an answer to us: we ask nothing
|
|
32
|
+
const isRequest = "id" in m;
|
|
33
|
+
if (m.jsonrpc !== "2.0" || typeof m.method !== "string") { if (isRequest) error(m.id ?? null, INVALID, "not a JSON-RPC 2.0 request"); return; }
|
|
34
|
+
const params = (typeof m.params === "object" && m.params !== null ? m.params : {}) as Record<string, unknown>;
|
|
35
|
+
if (!isRequest) { notified(m.method, params); return; }
|
|
36
|
+
const id = m.id as Id;
|
|
37
|
+
const p = handle(m.method, params)
|
|
38
|
+
.then((result) => send({ jsonrpc: "2.0", id, result: result ?? {} }))
|
|
39
|
+
.catch((err) => err instanceof RpcError ? error(id, err.code, err.message, err.data) : error(id, INTERNAL, err instanceof Error ? err.message : String(err)))
|
|
40
|
+
.finally(() => pending.delete(p));
|
|
41
|
+
pending.add(p);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const done = (async () => {
|
|
45
|
+
const decoder = new TextDecoder();
|
|
46
|
+
let buf = "";
|
|
47
|
+
const line = (l: string) => {
|
|
48
|
+
if (!l.trim()) return;
|
|
49
|
+
let msg: unknown;
|
|
50
|
+
try { msg = JSON.parse(l); } catch { error(null, PARSE, "not JSON"); return; }
|
|
51
|
+
if (Array.isArray(msg)) { for (const x of msg) one(x); return; }
|
|
52
|
+
one(msg);
|
|
53
|
+
};
|
|
54
|
+
for await (const chunk of input) {
|
|
55
|
+
buf += decoder.decode(chunk, { stream: true });
|
|
56
|
+
let nl: number;
|
|
57
|
+
while ((nl = buf.indexOf("\n")) >= 0) { line(buf.slice(0, nl)); buf = buf.slice(nl + 1); }
|
|
58
|
+
}
|
|
59
|
+
line(buf + decoder.decode());
|
|
60
|
+
while (pending.size) await Promise.all([...pending]);
|
|
61
|
+
})();
|
|
62
|
+
|
|
63
|
+
return { notify: (method, params) => send({ jsonrpc: "2.0", method, ...(params ? { params } : {}) }), done };
|
|
64
|
+
}
|
package/src/mcp/tools.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// The contract as MCP tools. Pure: a manifest in, the tool list out. By
|
|
2
|
+
// default six fixed tools, the same operations the CLI has over the one core
|
|
3
|
+
// (door/core.ts): `manifest`, `call`, `read`, `follow`, `watch`, `state`. The
|
|
4
|
+
// contract is not in the tool list, since harnesses do not reliably pick up a
|
|
5
|
+
// changed one; `manifest` teaches it. With VOSS_MCP_TOOLS=1, one tool per
|
|
6
|
+
// action and query too, `<system>_<name>`, with its summary and schemas.
|
|
7
|
+
import type { Manifest } from "../ecs/baba.ts";
|
|
8
|
+
|
|
9
|
+
/** A JSON Schema, as the manifest carries it. */
|
|
10
|
+
type Obj = Record<string, unknown>;
|
|
11
|
+
|
|
12
|
+
export interface Tool { name: string; description: string; inputSchema: Obj; outputSchema?: Obj }
|
|
13
|
+
|
|
14
|
+
/** What a per-call tool stands for. */
|
|
15
|
+
export interface Target { kind: "action" | "query"; name: string; system: string }
|
|
16
|
+
|
|
17
|
+
export interface Tools { tools: Tool[]; targets: Map<string, Target>; generic: boolean }
|
|
18
|
+
|
|
19
|
+
/** The promptware's threshold for describing the contract as `call`/`read`; the server itself is generic unless VOSS_MCP_TOOLS=1. */
|
|
20
|
+
|
|
21
|
+
/** How an agent learns the contract: said in every tool that takes a call's name. */
|
|
22
|
+
const LEARN = "`manifest` lists every call with an example; `manifest` with a name gives its schemas.";
|
|
23
|
+
|
|
24
|
+
/** `<system>_<name>`; a name that already starts with its system's, as voss's own do (`promptware-sync`), is not prefixed twice: `promptware_sync`. */
|
|
25
|
+
export const toolName = (system: string, name: string) => name.startsWith(`${system}-`) ? `${system}_${name.slice(system.length + 1)}` : `${system}_${name}`;
|
|
26
|
+
|
|
27
|
+
const isObj = (v: unknown): v is Obj => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
28
|
+
|
|
29
|
+
/** An args schema as MCP takes it: always an object schema. */
|
|
30
|
+
function objectSchema(j: unknown): Obj {
|
|
31
|
+
if (isObj(j) && j.type === "object") return j;
|
|
32
|
+
if (isObj(j) && j.type === undefined && isObj(j.properties)) return { ...j, type: "object" };
|
|
33
|
+
return { type: "object", properties: {} };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** An action's tool also takes `_key`: an idempotency key, so a retry answers the same. */
|
|
37
|
+
const withKey = (j: Obj): Obj => ({ ...j, properties: { ...(isObj(j.properties) ? j.properties : {}), _key: { type: "string", description: "an idempotency key: a retry with the same key answers the same" } } });
|
|
38
|
+
|
|
39
|
+
const forSecs: Obj = { type: "number", minimum: 0, maximum: 600, description: "seconds to wait at most; 30 by default" };
|
|
40
|
+
|
|
41
|
+
/** The six: the same operations as the CLI's, whatever the baba declares. */
|
|
42
|
+
export const genericTools: Tool[] = [
|
|
43
|
+
{
|
|
44
|
+
name: "manifest",
|
|
45
|
+
description: `The contract. With nothing, the index: every action and query with its kind, system, summary and an example to copy, the components with their key fields, the resources, where the state is. \`system\` narrows the calls; \`name\` answers that one call's args and result schemas and its example. ${LEARN}`,
|
|
46
|
+
inputSchema: { type: "object", properties: { system: { type: "string" }, name: { type: "string" } }, additionalProperties: false },
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "call",
|
|
50
|
+
description: `Call an action by name with its args (a query's name reads it). A \`key\` makes a retry answer the same. An action that starts work answers the entity to \`follow\`. ${LEARN}`,
|
|
51
|
+
inputSchema: { type: "object", properties: { name: { type: "string" }, args: { type: "object" }, key: { type: "string", description: "an idempotency key: a retry with the same key answers the same" } }, required: ["name"], additionalProperties: false },
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "read",
|
|
55
|
+
description: `Read a query by name with its args. ${LEARN}`,
|
|
56
|
+
inputSchema: { type: "object", properties: { name: { type: "string" }, args: { type: "object" } }, required: ["name"], additionalProperties: false },
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "follow",
|
|
60
|
+
description: "Wait on an entity, by `entity` or by `component` and its `key`, and answer its row: until it has COMPONENT, or COMPONENT.field=value holds, or (without `until`) until no job runs for it and the state has moved past it; bounded by `for`. A timed-out answer carries `timedOut`, a gone entity `gone`; both are errors.",
|
|
61
|
+
inputSchema: { type: "object", properties: { entity: { type: "integer", minimum: 1, description: "an entity" }, component: { type: "string", description: "with key: a component with a key field" }, key: { type: ["string", "number"], description: "the entity whose component's key field is this" }, until: { type: "string", description: "COMPONENT, or COMPONENT.field=value" }, for: forSecs }, additionalProperties: false },
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "watch",
|
|
65
|
+
description: `Read a query, then wait until its answer differs from that first read, bounded by \`for\`; answers {value}, or {value, timedOut: true} with the last read. ${LEARN}`,
|
|
66
|
+
inputSchema: { type: "object", properties: { name: { type: "string", description: "the query's name" }, args: { type: "object", description: "the query's args" }, for: forSecs }, required: ["name"], additionalProperties: false },
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "state",
|
|
70
|
+
description: "Read the state. With nothing, a summary: entities per component, the resources, jobs per status, the round; `all` the whole of it. With `entity`, that entity; with `component`, its rows filtered by `where` (field: value), at most `limit`, or with `key` the one whose key field it is; a resource's name as `component` answers its value.",
|
|
71
|
+
inputSchema: { type: "object", properties: { component: { type: "string" }, entity: { type: "integer", minimum: 1 }, key: { type: ["string", "number"] }, where: { type: "object", additionalProperties: true }, limit: { type: "integer", minimum: 1, description: "100 by default" }, all: { type: "boolean" }, deep: { type: "boolean", description: "entity refs expanded into their rows" } }, additionalProperties: false },
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
/** Every action and query, in manifest order. */
|
|
76
|
+
export function calls(m: Manifest): (Target & { summary: string; args: Obj; result: Obj })[] {
|
|
77
|
+
return [
|
|
78
|
+
...Object.entries(m.actions).map(([name, c]) => ({ kind: "action" as const, name, system: c.owner, summary: c.summary, args: c.args, result: c.result })),
|
|
79
|
+
...Object.entries(m.queries).map(([name, c]) => ({ kind: "query" as const, name, system: c.owner, summary: c.summary, args: c.args, result: c.result })),
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The six, and with `perCall` one tool per action and query before them. */
|
|
84
|
+
export function toolsOf(m: Manifest, opts: { perCall?: boolean } = {}): Tools {
|
|
85
|
+
const generic = opts.perCall !== true;
|
|
86
|
+
const targets = new Map<string, Target>();
|
|
87
|
+
if (generic) return { tools: genericTools, targets, generic };
|
|
88
|
+
const all = calls(m);
|
|
89
|
+
const tools: Tool[] = all.map((c) => {
|
|
90
|
+
const name = toolName(c.system, c.name);
|
|
91
|
+
targets.set(name, { kind: c.kind, name: c.name, system: c.system });
|
|
92
|
+
return {
|
|
93
|
+
name,
|
|
94
|
+
description: c.summary || `${c.kind} of ${c.system}`,
|
|
95
|
+
inputSchema: c.kind === "action" ? withKey(objectSchema(c.args)) : objectSchema(c.args),
|
|
96
|
+
...(isObj(c.result) && c.result.type === "object" ? { outputSchema: c.result } : {}),
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
return { tools: [...tools, ...genericTools], targets, generic };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** An action's or query's name from itself or its per-call tool's name. */
|
|
103
|
+
export function callName(m: Manifest, n: string): { kind: "action" | "query"; name: string } | null {
|
|
104
|
+
for (const c of calls(m)) if (c.name === n || toolName(c.system, c.name) === n) return { kind: c.kind, name: c.name };
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** One prompt per skill, by its name. */
|
|
109
|
+
export function promptsOf(m: Manifest): { name: string; description: string; skill: string }[] {
|
|
110
|
+
return Object.entries(m.promptware.skills).map(([k, sk]) => ({ name: k, description: sk.description, skill: k }));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** A skill's prompt text: its body, then the tools it uses. */
|
|
114
|
+
export function promptText(m: Manifest, skill: string, generic: boolean): string | null {
|
|
115
|
+
const sk = m.promptware.skills[skill];
|
|
116
|
+
if (!sk) return null;
|
|
117
|
+
const uses = (sk.uses ?? []).map((n) => {
|
|
118
|
+
const c = m.actions[n] ?? m.queries[n];
|
|
119
|
+
if (!c) return `- ${n}`;
|
|
120
|
+
const kind = n in m.actions ? "action" : "query";
|
|
121
|
+
const tool = generic ? `${kind === "query" ? "read" : "call"} {"name":"${n}"}` : toolName(c.owner, n);
|
|
122
|
+
return `- ${tool}: ${c.summary || `${kind} of ${c.owner}`}`;
|
|
123
|
+
});
|
|
124
|
+
return uses.length ? `${sk.body.trimEnd()}\n\nCalls:\n${uses.join("\n")}\n` : sk.body;
|
|
125
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// The golden interpretation of a piece of promptware: situations a fresh
|
|
2
|
+
// reader must decide from the text alone. Authored as givens, each with the
|
|
3
|
+
// cases under it: when (the moment of decision) and then (the right move, a
|
|
4
|
+
// few words). A situation the text never mentions, decided by what the text
|
|
5
|
+
// conveys, is the point: the text gives the principle, the cases sample what
|
|
6
|
+
// follows from it. Owned by the author through git; the trials system scores
|
|
7
|
+
// readers against it and never writes it. Under the hood each case is one
|
|
8
|
+
// flat record, given carried along, so the reader and the judge see one list.
|
|
9
|
+
export interface InterpretationCase { id: string; when: string; then: string }
|
|
10
|
+
export interface InterpretationGiven { given: string; cases: InterpretationCase[] }
|
|
11
|
+
export interface InterpretationQuestion { id: string; given: string; when: string; then: string }
|
|
12
|
+
export interface InterpretationSuite { questions: InterpretationQuestion[] }
|
|
13
|
+
|
|
14
|
+
/** Givens with their cases, flattened: one record per case, in order. */
|
|
15
|
+
export function interpretation(givens: InterpretationGiven[]): InterpretationSuite {
|
|
16
|
+
const questions: InterpretationQuestion[] = [];
|
|
17
|
+
const ids = new Set<string>();
|
|
18
|
+
for (const g of givens) {
|
|
19
|
+
if (!g.given.trim()) throw new Error("interpretation: a given is empty");
|
|
20
|
+
if (!g.cases.length) throw new Error(`interpretation: no cases under ${JSON.stringify(g.given)}`);
|
|
21
|
+
for (const c of g.cases) {
|
|
22
|
+
if (!/^[a-z][a-z0-9-]*$/.test(c.id)) throw new Error(`interpretation: an id is lowercase words joined by dashes: ${JSON.stringify(c.id)}`);
|
|
23
|
+
if (ids.has(c.id)) throw new Error(`interpretation: two cases are named ${c.id}`);
|
|
24
|
+
ids.add(c.id);
|
|
25
|
+
if (!c.when.trim() || !c.then.trim()) throw new Error(`interpretation ${c.id}: a when and a then`);
|
|
26
|
+
questions.push({ id: c.id, given: g.given.trim(), when: c.when.trim(), then: c.then.trim() });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (!questions.length) throw new Error("interpretation: at least one situation");
|
|
30
|
+
return { questions };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The flat records back as givens with their cases, consecutive records of one given together: how a surface draws them. */
|
|
34
|
+
export function givensOf(questions: readonly InterpretationQuestion[]): InterpretationGiven[] {
|
|
35
|
+
const out: InterpretationGiven[] = [];
|
|
36
|
+
for (const q of questions) {
|
|
37
|
+
const last = out[out.length - 1];
|
|
38
|
+
if (last && last.given === q.given) last.cases.push({ id: q.id, when: q.when, then: q.then });
|
|
39
|
+
else out.push({ given: q.given, cases: [{ id: q.id, when: q.when, then: q.then }] });
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|