agentus 0.0.0-stage → 0.2.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.
Files changed (59) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +11 -0
  3. package/README.md +476 -2
  4. package/README.zh-CN.md +396 -0
  5. package/bin/agentus.mjs +147 -0
  6. package/docs/android-notify-contract.md +658 -0
  7. package/docs/dev/01-dev-loop.md +186 -0
  8. package/docs/dev/02-verify.md +237 -0
  9. package/docs/dev/03-release.md +218 -0
  10. package/docs/dev/04-recover.md +199 -0
  11. package/docs/dev/05-agent-driven.md +114 -0
  12. package/docs/dev/README.md +65 -0
  13. package/docs/installation.md +366 -0
  14. package/docs/installation.zh-CN.md +350 -0
  15. package/docs/refs/aionui-acp-rules.md +46 -0
  16. package/docs/refs/hermes-studio-ui-ideas.md +191 -0
  17. package/docs/transcript-folding.md +139 -0
  18. package/package.json +76 -5
  19. package/packages/server/mcp/plan-server.mjs +217 -0
  20. package/packages/server/mock/agent.mjs +693 -0
  21. package/packages/server/package.json +22 -0
  22. package/packages/server/src/acp/backends.ts +196 -0
  23. package/packages/server/src/acp/errors.ts +46 -0
  24. package/packages/server/src/acp/registry.ts +579 -0
  25. package/packages/server/src/acp/session-manager.ts +1868 -0
  26. package/packages/server/src/auth.ts +688 -0
  27. package/packages/server/src/dashscope.ts +235 -0
  28. package/packages/server/src/fs.ts +379 -0
  29. package/packages/server/src/hotwords.ts +132 -0
  30. package/packages/server/src/index.ts +1486 -0
  31. package/packages/server/src/notify/center.ts +994 -0
  32. package/packages/server/src/notify/types.ts +222 -0
  33. package/packages/server/src/plan/plan.ts +126 -0
  34. package/packages/server/src/pty-helper.py +135 -0
  35. package/packages/server/src/settings.ts +469 -0
  36. package/packages/server/src/store/export.ts +209 -0
  37. package/packages/server/src/store/store.ts +1238 -0
  38. package/packages/server/src/term.ts +201 -0
  39. package/packages/server/src/title.ts +109 -0
  40. package/packages/server/src/voice.ts +251 -0
  41. package/packages/shared/package.json +14 -0
  42. package/packages/shared/src/index.ts +271 -0
  43. package/packages/web/dist/assets/index-D667PX2c.css +32 -0
  44. package/packages/web/dist/assets/index-d9NzwPFu.js +283 -0
  45. package/packages/web/dist/coding-agents/hermes.png +0 -0
  46. package/packages/web/dist/coding-agents/qoder.svg +5 -0
  47. package/packages/web/dist/icons/agentus-icon-inverse.svg +301 -0
  48. package/packages/web/dist/icons/agentus-icon.svg +301 -0
  49. package/packages/web/dist/icons/agentus-mark-inverse.svg +301 -0
  50. package/packages/web/dist/icons/agentus-mark.svg +301 -0
  51. package/packages/web/dist/icons/apple-touch-icon-180.png +0 -0
  52. package/packages/web/dist/icons/favicon.ico +0 -0
  53. package/packages/web/dist/icons/icon-192.png +0 -0
  54. package/packages/web/dist/icons/icon-512.png +0 -0
  55. package/packages/web/dist/icons/icon-maskable-512.png +0 -0
  56. package/packages/web/dist/index.html +30 -0
  57. package/packages/web/dist/manifest.webmanifest +17 -0
  58. package/packages/web/dist/sw.js +61 -0
  59. package/scripts/make-cert.sh +101 -0
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@agentus/server",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "tsx watch src/index.ts",
8
+ "start": "tsx src/index.ts",
9
+ "typecheck": "tsc --noEmit"
10
+ },
11
+ "dependencies": {
12
+ "@agentclientprotocol/sdk": "1.5.1",
13
+ "@agentus/shared": "*",
14
+ "ws": "^8.22.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^22.0.0",
18
+ "@types/ws": "^8.18.2",
19
+ "tsx": "^4.19.0",
20
+ "typescript": "^5.6.0"
21
+ }
22
+ }
@@ -0,0 +1,196 @@
1
+ // Backend registry — the entire multi-backend support surface (D5).
2
+ // Adding a backend = adding one row here; NEVER touch the protocol layer.
3
+ //
4
+ // Home rule: a backend that owns a home directory always gets an EXPLICIT HERMES_HOME — the
5
+ // row's own if it names one, otherwise the operator's real home (~/.hermes). An inherited
6
+ // HERMES_HOME must never decide where a slot writes, because a Hermes-launched terminal hands
7
+ // its own (the live one) down to us.
8
+ //
9
+ // The early-dev isolation guard — a hard error whenever a row pointed at the live home, plus
10
+ // the AGENTUS_ALLOW_LIVE_HOME escape hatch and the row's `allowLiveHome` tick — is GONE
11
+ // (2026-10-06): the cockpit is meant to drive the operator's real runtime. It existed because
12
+ // two openers on one WAL SQLite 3.50.4 (inside the WAL-reset range) corrupted the user's
13
+ // state.db; the runtime's interpreter now links SQLite 3.53.1, so the reason is gone. A row
14
+ // that wants its own data directory still simply names one.
15
+ import fs from "node:fs";
16
+ import { homedir } from "node:os";
17
+ import { fileURLToPath } from "node:url";
18
+ import path from "node:path";
19
+
20
+ export interface HomeIsolation {
21
+ /** env var naming the agent's home (state.db + friends live under it) */
22
+ homeVar: string;
23
+ /** where the home goes when the row does not name one — the operator's real home */
24
+ homeDefault: string;
25
+ }
26
+
27
+ export interface BackendSpec {
28
+ /** registry row id (M6); the builtin seeds carry theirs, spawn does not need it */
29
+ id?: string;
30
+ /** spawn family, so callers can reason about the arg shape without matching on the id */
31
+ kind?: "hermes" | "qoder" | "mock";
32
+ /**
33
+ * Where this agent's PLAN comes from — a declaration, not per-agent code.
34
+ *
35
+ * · `acp` — the agent emits ACP `plan` frames itself (Hermes does; that is the whole point of
36
+ * the fork fix). The cockpit ingests them and must NOT hand the agent a second
37
+ * writable list, so no plan MCP server is injected into the handshake.
38
+ * · `none` — anything else. The cockpit injects the `agentus-plan` MCP server at handshake
39
+ * time, so a new agent gets plan cards without one line of its own code.
40
+ *
41
+ * The choice is per SESSION (it rides the handshake), which is why it can never leak into the
42
+ * agent's own config — see design-plan-service.md §4.
43
+ */
44
+ nativePlanSource?: NativePlanSource;
45
+ label: string;
46
+ cmd: string;
47
+ args: string[];
48
+ check: string[] | null; // args for a quick `--check` self test
49
+ needsLoginHint?: string;
50
+ isolation?: HomeIsolation;
51
+ /** Extra env for the child — e.g. `PYTHONPATH=<the operator's fork tree>` so a row can run
52
+ * a source checkout while the box's default `hermes` runs the installed runtime. Merged
53
+ * BEFORE the isolation home, so a row cannot smuggle `HERMES_HOME` in through here. */
54
+ env?: Record<string, string>;
55
+ /** hermes profile, composed into argv by registry.effectiveArgs (`hermes -p <profile> acp`) */
56
+ profile?: string | null;
57
+ /** free-form note shown next to the row in the UI */
58
+ notes?: string;
59
+ }
60
+
61
+ /** The operator's real home — the DEFAULT for a hermes row, not a禁区. */
62
+ const liveHermesHome = path.join(homedir(), ".hermes");
63
+ const defaultHermesHome = process.env.AGENTUS_HERMES_HOME || liveHermesHome;
64
+
65
+ export const BACKENDS: Record<string, BackendSpec> = {
66
+ hermes: {
67
+ label: "Hermes",
68
+ cmd: process.env.AGENTUS_HERMES_CMD || "hermes",
69
+ args: ["acp"],
70
+ check: ["acp", "--check"],
71
+ // Hermes CAN emit ACP `plan` frames (acp_adapter/events.py), but the cockpit deliberately does
72
+ // NOT use them for the card: an agent's own todo state dies with its process, so a restart used
73
+ // to leave the card lying about work that no longer exists anywhere (design-plan-service.md §1).
74
+ // Hermes is therefore MCP-driven like everything else, and its frames are ignored (§5 of the
75
+ // same doc). `nativePlanSource: "acp"` survives only as a PER-ROW escape hatch.
76
+ nativePlanSource: "none",
77
+ isolation: {
78
+ homeVar: "HERMES_HOME",
79
+ homeDefault: defaultHermesHome,
80
+ },
81
+ },
82
+ qoder: {
83
+ label: "Qoder",
84
+ cmd:
85
+ process.env.AGENTUS_QODER_CMD ||
86
+ path.join(homedir(), ".local/bin/qodercli"),
87
+ args: ["--acp"],
88
+ check: null,
89
+ needsLoginHint: "qodercli login",
90
+ // qodercli keeps its own state under ~/.qoder; no home env to pin yet.
91
+ // Nothing known to emit plan frames → hand it the plan MCP tool instead (zero adapter code).
92
+ nativePlanSource: "none",
93
+ },
94
+ mock: {
95
+ label: "Mock Agent",
96
+ cmd: process.env.AGENTUS_MOCK_CMD || "node",
97
+ // resolve mock relative to this file, not process.cwd() (start.sh launches from the
98
+ // repo root, the dev server from packages/server); fileURLToPath so a clone inside a
99
+ // path with spaces ("~/My Projects/…") doesn't arrive percent-encoded
100
+ args: [
101
+ path.join(
102
+ path.dirname(fileURLToPath(import.meta.url)),
103
+ "../../mock/agent.mjs",
104
+ ),
105
+ ],
106
+ check: null,
107
+ nativePlanSource: "none",
108
+ },
109
+ };
110
+
111
+ export type BackendId = keyof typeof BACKENDS;
112
+
113
+ /** Which channel carries a backend's plan. See BackendSpec.nativePlanSource.
114
+ *
115
+ * `none` is the default AND the shipping configuration for every backend: the cockpit's plan
116
+ * object is the only plan that survives an agent restart, so the `agentus-plan` MCP tool is what
117
+ * writes (and what the agent is told to prefer). `acp` is a per-row escape hatch for an agent whose
118
+ * frames you have decided to trust more than the object — it is the ONLY case in which frames are
119
+ * rendered. */
120
+ export type NativePlanSource = "acp" | "none";
121
+
122
+ /** What a backend's plan channel is when its row does not say. One constant, so the registry's
123
+ * derivation, the built-in specs and the session manager cannot drift apart. */
124
+ export const DEFAULT_NATIVE_PLAN_SOURCE: NativePlanSource = "none";
125
+
126
+ /** The channel a spec resolves to, defaulting rather than throwing on an absent value. */
127
+ export function nativePlanSourceOf(spec: { nativePlanSource?: NativePlanSource }): NativePlanSource {
128
+ return spec.nativePlanSource ?? DEFAULT_NATIVE_PLAN_SOURCE;
129
+ }
130
+
131
+ export function expandHome(p: string): string {
132
+ return p.startsWith("~") ? path.join(homedir(), p.slice(1)) : p;
133
+ }
134
+
135
+ export interface SpawnPlan {
136
+ env: NodeJS.ProcessEnv;
137
+ /** resolved home we handed the child (null when the backend has none) */
138
+ home: string | null;
139
+ /** non-fatal isolation concerns worth surfacing in the UI */
140
+ warnings: string[];
141
+ }
142
+
143
+ /**
144
+ * Build the child env for a backend. Always sets the home env var explicitly — the row's home,
145
+ * or the operator's real home when the row does not name one. Nothing else is policed: driving
146
+ * the operator's own runtime is the point of the cockpit.
147
+ */
148
+ export function buildSpawnEnv(spec: BackendSpec): SpawnPlan {
149
+ // A row's extra env (e.g. PYTHONPATH choosing the fork tree) is merged FIRST; the home
150
+ // below overwrites `HERMES_HOME` unconditionally, so no row can smuggle a home in.
151
+ const env: NodeJS.ProcessEnv = { ...process.env, ...(spec.env ?? {}) };
152
+ const warnings: string[] = [];
153
+ if (!spec.isolation) return { env, home: null, warnings };
154
+
155
+ const { homeVar, homeDefault } = spec.isolation;
156
+ // The parent env is untrusted here: a Hermes-launched terminal hands its own
157
+ // HERMES_HOME (the live one) down to us, and inheriting that silently is how a slot ends up
158
+ // writing somewhere nobody chose. Only the row — or the AGENTUS_HERMES_HOME seed behind its
159
+ // default — decides; an ambient HERMES_HOME is reported, never obeyed.
160
+ const ambient = process.env[homeVar] ? expandHome(process.env[homeVar]!) : null;
161
+ const home = path.resolve(expandHome(homeDefault));
162
+ if (ambient && path.resolve(ambient) !== home) {
163
+ warnings.push(
164
+ `ignored inherited ${homeVar}=${ambient} (parent env, not authoritative); ` +
165
+ `child gets ${home}. Set a home on the backend row to choose a different one.`,
166
+ );
167
+ }
168
+ env[homeVar] = home;
169
+
170
+ // The operator's real home is the DEFAULT, so it says nothing. A DIFFERENT home is the case
171
+ // worth a warning: Hindsight's embedded Postgres instance is keyed off the *agent* config
172
+ // (~/.pg0/instances/hindsight-embed-<profile>, profile from that home's hindsight/config.json,
173
+ // default "hermes"), so a separate home that keeps the default name would attach to the
174
+ // production memory daemon.
175
+ if (home !== path.resolve(liveHermesHome)) {
176
+ try {
177
+ const cfgPath = path.join(home, "hindsight", "config.json");
178
+ if (fs.existsSync(cfgPath)) {
179
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8")) as {
180
+ profile?: string;
181
+ };
182
+ const name = cfg.profile || "hermes";
183
+ if (name === "hermes") {
184
+ warnings.push(
185
+ `Hindsight instance name is the shared default "hermes" — this home ` +
186
+ `would reuse the live memory daemon (${path.join(homedir(), ".pg0/instances/hindsight-embed-hermes")}). ` +
187
+ `Set {"profile":"agentus"} in ${cfgPath} or disable memory.`,
188
+ );
189
+ }
190
+ }
191
+ } catch {
192
+ /* config unreadable → not worth failing the spawn over */
193
+ }
194
+ }
195
+ return { env, home, warnings };
196
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * ACP errors put the actionable text in the JSON-RPC error's `data`, while `message` stays the
3
+ * protocol title ("Invalid params" / "Internal error"). Hermes does exactly that for a rejected
4
+ * model switch — `RequestError.invalid_params({"details": str(exc)})`, where `details` is the
5
+ * sentence that says WHY (`was not found in this provider's model listing. Similar models: …`)
6
+ * — and the SDK preserves it (`new RequestError(code, message, data)`, jsonrpc.js).
7
+ *
8
+ * Stringifying `.message` alone (what this server used to do) therefore threw the diagnosis away
9
+ * and left the operator with four useless words: measured on the live cockpit, a model that could
10
+ * not be switched reported `model switch failed: Invalid params` and nothing else (track §61).
11
+ * Every boundary that reports an agent-side failure goes through here instead.
12
+ *
13
+ * When all `.message` holds is a protocol title, the details ARE the error and are returned
14
+ * alone — "Invalid params: Model `x` was not found …" leads with noise the operator cannot act
15
+ * on. A message with content of its own keeps the title out front so the two stay distinguishable.
16
+ */
17
+ const PROTOCOL_TITLES = new Set([
18
+ "parse error",
19
+ "invalid request",
20
+ "method not found",
21
+ "invalid params",
22
+ "internal error",
23
+ "request cancelled",
24
+ "authentication required",
25
+ "resource not found",
26
+ ]);
27
+
28
+ export function describeAcpError(e: unknown): string {
29
+ const message = String((e as { message?: unknown })?.message ?? e);
30
+ const details = acpErrorDetails((e as { data?: unknown })?.data);
31
+ if (!details || details === message || message.includes(details)) return message;
32
+ return PROTOCOL_TITLES.has(message.trim().toLowerCase()) ? details : `${message}: ${details}`;
33
+ }
34
+
35
+ /** The human sentence inside a JSON-RPC error `data` payload, or "" when there is none we trust
36
+ * to be short and readable (an arbitrary object would drown the banner in JSON). */
37
+ function acpErrorDetails(data: unknown): string {
38
+ if (typeof data === "string") return data.trim();
39
+ if (typeof data !== "object" || data === null) return "";
40
+ const rec = data as Record<string, unknown>;
41
+ for (const key of ["details", "detail", "error", "message"]) {
42
+ const v = rec[key];
43
+ if (typeof v === "string" && v.trim()) return v.trim();
44
+ }
45
+ return "";
46
+ }