@botcord/daemon 0.2.88 → 0.2.90
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/dist/cloud-daemon.js +18 -4
- package/dist/daemon.d.ts +2 -3
- package/dist/daemon.js +21 -6
- package/dist/gateway/runtimes/gemini.d.ts +57 -5
- package/dist/gateway/runtimes/gemini.js +266 -5
- package/dist/gateway/runtimes/registry.d.ts +1 -1
- package/dist/gateway/runtimes/registry.js +3 -2
- package/dist/provision.d.ts +1 -0
- package/dist/provision.js +83 -7
- package/dist/runtime-models.js +46 -0
- package/dist/skill-index.d.ts +14 -2
- package/dist/skill-index.js +81 -34
- package/dist/skill-installer.d.ts +61 -0
- package/dist/skill-installer.js +340 -0
- package/dist/system-context.d.ts +6 -0
- package/dist/system-context.js +1 -1
- package/package.json +3 -3
- package/src/__tests__/runtime-discovery.test.ts +6 -3
- package/src/__tests__/runtime-models.test.ts +53 -0
- package/src/__tests__/skill-index.test.ts +89 -13
- package/src/__tests__/skill-installer.test.ts +224 -0
- package/src/cloud-daemon.ts +17 -4
- package/src/daemon.ts +23 -8
- package/src/gateway/__tests__/gemini-adapter.test.ts +357 -0
- package/src/gateway/runtimes/gemini.ts +301 -7
- package/src/gateway/runtimes/registry.ts +4 -2
- package/src/provision.ts +90 -12
- package/src/runtime-models.ts +47 -0
- package/src/skill-index.ts +103 -47
- package/src/skill-installer.ts +472 -0
- package/src/system-context.ts +7 -1
package/dist/cloud-daemon.js
CHANGED
|
@@ -76,6 +76,15 @@ export async function startCloudDaemon(opts) {
|
|
|
76
76
|
const credentialPathByAgentId = new Map();
|
|
77
77
|
const hubUrlByAgentId = new Map();
|
|
78
78
|
const displayNameByAgent = new Map();
|
|
79
|
+
const runtimeByAgentId = new Map();
|
|
80
|
+
const hermesProfileByAgentId = new Map();
|
|
81
|
+
const skillIndexOptionsForAgent = (agentId) => {
|
|
82
|
+
const hermesProfile = hermesProfileByAgentId.get(agentId);
|
|
83
|
+
return {
|
|
84
|
+
runtime: runtimeByAgentId.get(agentId) ?? opts.config.defaultRoute.adapter,
|
|
85
|
+
...(hermesProfile ? { hermesProfile } : {}),
|
|
86
|
+
};
|
|
87
|
+
};
|
|
79
88
|
// Seed each per-agent hub URL with the cloud-mode value so that even
|
|
80
89
|
// before the first credential file is written the room-context fetcher
|
|
81
90
|
// has somewhere sensible to point.
|
|
@@ -158,16 +167,16 @@ export async function startCloudDaemon(opts) {
|
|
|
158
167
|
});
|
|
159
168
|
};
|
|
160
169
|
const installedAgentIds = new Set();
|
|
161
|
-
const runtimeByAgentId = new Map();
|
|
162
170
|
let controlChannel = null;
|
|
163
171
|
const pushInstalledAgentSkillSnapshot = (agentId, reason) => {
|
|
164
172
|
if (!controlChannel)
|
|
165
173
|
return;
|
|
166
|
-
const
|
|
167
|
-
const pushed = pushAgentSkillSnapshot(controlChannel, agentId,
|
|
174
|
+
const skillIndexOptions = skillIndexOptionsForAgent(agentId);
|
|
175
|
+
const pushed = pushAgentSkillSnapshot(controlChannel, agentId, skillIndexOptions);
|
|
168
176
|
logger.info("cloud control-channel: agent_skill_snapshot pushed", {
|
|
169
177
|
agentId,
|
|
170
|
-
runtime,
|
|
178
|
+
runtime: skillIndexOptions.runtime,
|
|
179
|
+
hermesProfile: skillIndexOptions.hermesProfile ?? null,
|
|
171
180
|
reason,
|
|
172
181
|
ok: pushed,
|
|
173
182
|
});
|
|
@@ -176,6 +185,10 @@ export async function startCloudDaemon(opts) {
|
|
|
176
185
|
installedAgentIds.add(info.agentId);
|
|
177
186
|
if (info.runtime)
|
|
178
187
|
runtimeByAgentId.set(info.agentId, info.runtime);
|
|
188
|
+
if (info.hermesProfile)
|
|
189
|
+
hermesProfileByAgentId.set(info.agentId, info.hermesProfile);
|
|
190
|
+
else if (info.runtime)
|
|
191
|
+
hermesProfileByAgentId.delete(info.agentId);
|
|
179
192
|
credentialPathByAgentId.set(info.agentId, info.credentialsFile);
|
|
180
193
|
if (info.hubUrl)
|
|
181
194
|
hubUrlByAgentId.set(info.agentId, info.hubUrl);
|
|
@@ -186,6 +199,7 @@ export async function startCloudDaemon(opts) {
|
|
|
186
199
|
agentId: info.agentId,
|
|
187
200
|
activityTracker,
|
|
188
201
|
roomContextBuilder,
|
|
202
|
+
skillIndexOptions: () => skillIndexOptionsForAgent(info.agentId),
|
|
189
203
|
// Cloud daemons run isolated — no loop-risk guard wired in PR1;
|
|
190
204
|
// the runtime adapter's wall-time budget enforces the equivalent.
|
|
191
205
|
loopRiskBuilder: () => null,
|
package/dist/daemon.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { DaemonConfig } from "./config.js";
|
|
|
3
3
|
import { type BootAgentsResult } from "./agent-discovery.js";
|
|
4
4
|
import { ensureAgentWorkspace } from "./agent-workspace.js";
|
|
5
5
|
import { UserAuthManager } from "./user-auth.js";
|
|
6
|
+
import { type SkillIndexOptions } from "./skill-index.js";
|
|
6
7
|
import { classifyActivitySender } from "./sender-classify.js";
|
|
7
8
|
export { classifyActivitySender };
|
|
8
9
|
/** Minimal activity-tracker surface the inbound observer uses. */
|
|
@@ -65,9 +66,7 @@ export interface RuntimeSnapshotSink {
|
|
|
65
66
|
* or wait for the next daemon restart). Exported for unit tests.
|
|
66
67
|
*/
|
|
67
68
|
export declare function pushRuntimeSnapshot(sink: RuntimeSnapshotSink, liveSnapshot?: GatewayRuntimeSnapshot): boolean;
|
|
68
|
-
export declare function pushAgentSkillSnapshot(sink: RuntimeSnapshotSink, agentId: string, opts?:
|
|
69
|
-
runtime?: string;
|
|
70
|
-
}): boolean;
|
|
69
|
+
export declare function pushAgentSkillSnapshot(sink: RuntimeSnapshotSink, agentId: string, opts?: SkillIndexOptions): boolean;
|
|
71
70
|
/** Options accepted by {@link startDaemon} — the P0.5 compatibility shim. */
|
|
72
71
|
export interface DaemonRuntimeOptions {
|
|
73
72
|
config: DaemonConfig;
|
package/dist/daemon.js
CHANGED
|
@@ -221,6 +221,13 @@ export async function startDaemon(opts) {
|
|
|
221
221
|
const agentIds = boot.agents.map((a) => a.agentId);
|
|
222
222
|
const { credentialPathByAgentId, agentRuntimes } = backfillBootAgents(boot.agents, { logger });
|
|
223
223
|
const gwConfig = toGatewayConfig(opts.config, { agentIds, agentRuntimes });
|
|
224
|
+
const skillIndexOptionsForAgent = (agentId) => {
|
|
225
|
+
const meta = agentRuntimes[agentId];
|
|
226
|
+
return {
|
|
227
|
+
runtime: meta?.runtime ?? opts.config.defaultRoute.adapter,
|
|
228
|
+
...(meta?.hermesProfile ? { hermesProfile: meta.hermesProfile } : {}),
|
|
229
|
+
};
|
|
230
|
+
};
|
|
224
231
|
// Per-agent hub URL — read from each credential file at boot. Used to
|
|
225
232
|
// populate `BOTCORD_HUB` for runtime CLI subprocesses so the bundled
|
|
226
233
|
// `botcord` CLI talks to the same hub the agent is registered against,
|
|
@@ -273,6 +280,7 @@ export async function startDaemon(opts) {
|
|
|
273
280
|
activityTracker,
|
|
274
281
|
roomContextBuilder,
|
|
275
282
|
loopRiskBuilder,
|
|
283
|
+
skillIndexOptions: () => skillIndexOptionsForAgent(aid),
|
|
276
284
|
}));
|
|
277
285
|
}
|
|
278
286
|
const buildSystemContext = (message) => {
|
|
@@ -372,11 +380,16 @@ export async function startDaemon(opts) {
|
|
|
372
380
|
// next room-context fetch re-loads the BotCordClient against the new
|
|
373
381
|
// credential file.
|
|
374
382
|
credentialPathByAgentId.set(info.agentId, info.credentialsFile);
|
|
375
|
-
if (info.runtime) {
|
|
376
|
-
|
|
383
|
+
if (info.runtime || info.hermesProfile) {
|
|
384
|
+
const next = {
|
|
377
385
|
...(agentRuntimes[info.agentId] ?? {}),
|
|
378
|
-
runtime: info.runtime,
|
|
386
|
+
...(info.runtime ? { runtime: info.runtime } : {}),
|
|
387
|
+
...(info.hermesProfile ? { hermesProfile: info.hermesProfile } : {}),
|
|
379
388
|
};
|
|
389
|
+
if (info.runtime && !info.hermesProfile) {
|
|
390
|
+
delete next.hermesProfile;
|
|
391
|
+
}
|
|
392
|
+
agentRuntimes[info.agentId] = next;
|
|
380
393
|
}
|
|
381
394
|
if (info.hubUrl)
|
|
382
395
|
hubUrlByAgentId.set(info.agentId, info.hubUrl);
|
|
@@ -388,6 +401,7 @@ export async function startDaemon(opts) {
|
|
|
388
401
|
activityTracker,
|
|
389
402
|
roomContextBuilder,
|
|
390
403
|
loopRiskBuilder,
|
|
404
|
+
skillIndexOptions: () => skillIndexOptionsForAgent(info.agentId),
|
|
391
405
|
}));
|
|
392
406
|
}
|
|
393
407
|
};
|
|
@@ -507,11 +521,12 @@ export async function startDaemon(opts) {
|
|
|
507
521
|
ok: pushed,
|
|
508
522
|
});
|
|
509
523
|
for (const agentId of agentIds) {
|
|
510
|
-
const
|
|
511
|
-
const skillsPushed = pushAgentSkillSnapshot(controlChannel, agentId,
|
|
524
|
+
const skillIndexOptions = skillIndexOptionsForAgent(agentId);
|
|
525
|
+
const skillsPushed = pushAgentSkillSnapshot(controlChannel, agentId, skillIndexOptions);
|
|
512
526
|
logger.info("control-channel: initial agent_skill_snapshot push", {
|
|
513
527
|
agentId,
|
|
514
|
-
runtime,
|
|
528
|
+
runtime: skillIndexOptions.runtime,
|
|
529
|
+
hermesProfile: skillIndexOptions.hermesProfile ?? null,
|
|
515
530
|
ok: skillsPushed,
|
|
516
531
|
});
|
|
517
532
|
}
|
|
@@ -1,15 +1,67 @@
|
|
|
1
|
+
import { NdjsonStreamAdapter, type NdjsonEventCtx } from "./ndjson-stream.js";
|
|
1
2
|
import { type ProbeDeps } from "./probe.js";
|
|
2
|
-
import type {
|
|
3
|
+
import type { RuntimeProbeResult, RuntimeRunOptions } from "../types.js";
|
|
3
4
|
/** Resolve the Gemini CLI executable on PATH. */
|
|
4
5
|
export declare function resolveGeminiCommand(deps?: ProbeDeps): string | null;
|
|
5
6
|
/** Probe whether the Gemini CLI is installed and report its version. */
|
|
6
7
|
export declare function probeGemini(deps?: ProbeDeps): RuntimeProbeResult;
|
|
7
8
|
/**
|
|
8
|
-
* Gemini adapter
|
|
9
|
-
*
|
|
9
|
+
* Gemini adapter — spawns `gemini -p "<text>" --output-format stream-json
|
|
10
|
+
* --yolo` (with `--resume <sid>` for continuing sessions) and parses the
|
|
11
|
+
* newline-delimited JSON stream.
|
|
12
|
+
*
|
|
13
|
+
* stream-json event shape (abridged, sourced from `@google/gemini-cli`
|
|
14
|
+
* bundle `nonInteractiveCliAgentSession.ts`):
|
|
15
|
+
*
|
|
16
|
+
* {type:"init", timestamp, session_id, model}
|
|
17
|
+
* {type:"message", timestamp, role:"user", content} // echo of input
|
|
18
|
+
* {type:"message", timestamp, role:"assistant", content, delta:true}
|
|
19
|
+
* {type:"tool_use", timestamp, tool_name, tool_id, parameters}
|
|
20
|
+
* {type:"tool_result", timestamp, tool_id, status, output?, error?}
|
|
21
|
+
* {type:"error", timestamp, severity, message} // non-fatal warning
|
|
22
|
+
* {type:"result", timestamp, status:"success", stats} // terminal
|
|
23
|
+
* {type:"result", timestamp, status:"error", error:{type,message}, stats}
|
|
24
|
+
*
|
|
25
|
+
* Unlike Claude Code's `result` event, gemini's terminal event carries NO
|
|
26
|
+
* final assistant text — the reply must be assembled by concatenating every
|
|
27
|
+
* `message` event with `role:"assistant"` (the CLI emits them as deltas).
|
|
28
|
+
*
|
|
29
|
+
* ## systemContext
|
|
30
|
+
*
|
|
31
|
+
* Gemini's headless mode has no `--append-system-prompt` equivalent and
|
|
32
|
+
* `GEMINI_SYSTEM_MD` replaces the entire core system prompt (which would
|
|
33
|
+
* brick the agent — that core prompt scaffolds tool use). For v1 the
|
|
34
|
+
* adapter prepends `systemContext` directly to the positional prompt. Each
|
|
35
|
+
* turn re-injects the dynamic context so memory / digest updates take
|
|
36
|
+
* effect immediately; the trade-off is the resumed session transcript
|
|
37
|
+
* accumulates one prompt prefix per turn. Acceptable while we ship the
|
|
38
|
+
* connectivity layer — a follow-up can move systemContext into a
|
|
39
|
+
* daemon-managed `GEMINI.md` once we decide where to isolate it.
|
|
40
|
+
*
|
|
41
|
+
* ## Session continuity
|
|
42
|
+
*
|
|
43
|
+
* `gemini --session-id <uuid>` is for FRESH sessions only — it errors if
|
|
44
|
+
* the id already exists. `gemini --resume <uuid>` resolves the UUID against
|
|
45
|
+
* the project's existing session pool (gemini stores sessions per
|
|
46
|
+
* cwd-derived project hash, so the per-agent workspace already isolates
|
|
47
|
+
* them from the user's interactive sessions). We therefore:
|
|
48
|
+
* - new turn (sessionId=null): omit both flags; capture `init.session_id`.
|
|
49
|
+
* - continuation: pass `--resume <uuid>`. If gemini cannot resolve the id
|
|
50
|
+
* it exits with `FATAL_INPUT_ERROR` and stderr; we surface that as
|
|
51
|
+
* `errorText` and wipe `newSessionId` so the dispatcher discards the
|
|
52
|
+
* stale entry.
|
|
10
53
|
*/
|
|
11
|
-
export declare class GeminiAdapter
|
|
54
|
+
export declare class GeminiAdapter extends NdjsonStreamAdapter {
|
|
12
55
|
readonly id: "gemini";
|
|
56
|
+
private readonly explicitBinary;
|
|
57
|
+
private resolvedBinary;
|
|
58
|
+
constructor(opts?: {
|
|
59
|
+
binary?: string;
|
|
60
|
+
});
|
|
13
61
|
probe(): RuntimeProbeResult;
|
|
14
|
-
run(
|
|
62
|
+
run(opts: RuntimeRunOptions): Promise<import("../types.js").RuntimeRunResult>;
|
|
63
|
+
protected resolveBinary(): string;
|
|
64
|
+
protected buildArgs(opts: RuntimeRunOptions): string[];
|
|
65
|
+
protected spawnEnv(opts: RuntimeRunOptions): NodeJS.ProcessEnv;
|
|
66
|
+
protected handleEvent(raw: unknown, ctx: NdjsonEventCtx): void;
|
|
15
67
|
}
|
|
@@ -1,4 +1,79 @@
|
|
|
1
|
+
import { NdjsonStreamAdapter } from "./ndjson-stream.js";
|
|
1
2
|
import { readCommandVersion, resolveCommandOnPath, } from "./probe.js";
|
|
3
|
+
/**
|
|
4
|
+
* Gemini's `--session-id` / `--resume` accept only `[A-Za-z0-9-_]+` and we
|
|
5
|
+
* forward whatever the CLI emitted in its `init` event. Rejecting anything
|
|
6
|
+
* else keeps argv safe even if the upstream session id format ever changes.
|
|
7
|
+
*/
|
|
8
|
+
const GEMINI_SESSION_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
9
|
+
function isValidGeminiSessionId(id) {
|
|
10
|
+
return GEMINI_SESSION_ID_RE.test(id);
|
|
11
|
+
}
|
|
12
|
+
function invalidGeminiSessionIdError(id) {
|
|
13
|
+
return `gemini: invalid sessionId ${JSON.stringify(id)} (expected [A-Za-z0-9_-]+)`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Drop adapter-foreign flags inherited from other runtimes' route configs
|
|
17
|
+
* (claude-code, codex). Each entry that takes a value also swallows the
|
|
18
|
+
* value that follows. Anything else is forwarded verbatim so operators can
|
|
19
|
+
* still push gemini-native flags through.
|
|
20
|
+
*/
|
|
21
|
+
const GEMINI_FOREIGN_FLAGS_WITH_VALUE = new Set([
|
|
22
|
+
"--append-system-prompt",
|
|
23
|
+
"--permission-mode",
|
|
24
|
+
"--setting-sources",
|
|
25
|
+
"--sandbox",
|
|
26
|
+
"-c",
|
|
27
|
+
]);
|
|
28
|
+
const GEMINI_FOREIGN_BOOLEAN_FLAGS = new Set([
|
|
29
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
30
|
+
"--full-auto",
|
|
31
|
+
"--skip-git-repo-check",
|
|
32
|
+
"--json",
|
|
33
|
+
"--verbose",
|
|
34
|
+
]);
|
|
35
|
+
function extraFlagName(arg) {
|
|
36
|
+
if (!arg.startsWith("-"))
|
|
37
|
+
return arg;
|
|
38
|
+
const eq = arg.indexOf("=");
|
|
39
|
+
return eq === -1 ? arg : arg.slice(0, eq);
|
|
40
|
+
}
|
|
41
|
+
function nextExtraValue(args, index) {
|
|
42
|
+
const next = args[index + 1];
|
|
43
|
+
if (typeof next !== "string")
|
|
44
|
+
return undefined;
|
|
45
|
+
if (!next.startsWith("-"))
|
|
46
|
+
return next;
|
|
47
|
+
return /^-\d/.test(next) ? next : undefined;
|
|
48
|
+
}
|
|
49
|
+
function sanitizeGeminiExtraArgs(extraArgs) {
|
|
50
|
+
if (!extraArgs?.length)
|
|
51
|
+
return [];
|
|
52
|
+
const out = [];
|
|
53
|
+
for (let i = 0; i < extraArgs.length; i += 1) {
|
|
54
|
+
const arg = extraArgs[i];
|
|
55
|
+
const name = extraFlagName(arg);
|
|
56
|
+
if (GEMINI_FOREIGN_FLAGS_WITH_VALUE.has(name)) {
|
|
57
|
+
if (!arg.includes("=") && nextExtraValue(extraArgs, i) !== undefined)
|
|
58
|
+
i += 1;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (GEMINI_FOREIGN_BOOLEAN_FLAGS.has(name)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
out.push(arg);
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
function hasFlag(args, name) {
|
|
69
|
+
for (const arg of args) {
|
|
70
|
+
if (arg === name)
|
|
71
|
+
return true;
|
|
72
|
+
if (arg.startsWith(`${name}=`))
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
2
77
|
/** Resolve the Gemini CLI executable on PATH. */
|
|
3
78
|
export function resolveGeminiCommand(deps = {}) {
|
|
4
79
|
return resolveCommandOnPath("gemini", deps);
|
|
@@ -15,15 +90,201 @@ export function probeGemini(deps = {}) {
|
|
|
15
90
|
};
|
|
16
91
|
}
|
|
17
92
|
/**
|
|
18
|
-
* Gemini adapter
|
|
19
|
-
*
|
|
93
|
+
* Gemini adapter — spawns `gemini -p "<text>" --output-format stream-json
|
|
94
|
+
* --yolo` (with `--resume <sid>` for continuing sessions) and parses the
|
|
95
|
+
* newline-delimited JSON stream.
|
|
96
|
+
*
|
|
97
|
+
* stream-json event shape (abridged, sourced from `@google/gemini-cli`
|
|
98
|
+
* bundle `nonInteractiveCliAgentSession.ts`):
|
|
99
|
+
*
|
|
100
|
+
* {type:"init", timestamp, session_id, model}
|
|
101
|
+
* {type:"message", timestamp, role:"user", content} // echo of input
|
|
102
|
+
* {type:"message", timestamp, role:"assistant", content, delta:true}
|
|
103
|
+
* {type:"tool_use", timestamp, tool_name, tool_id, parameters}
|
|
104
|
+
* {type:"tool_result", timestamp, tool_id, status, output?, error?}
|
|
105
|
+
* {type:"error", timestamp, severity, message} // non-fatal warning
|
|
106
|
+
* {type:"result", timestamp, status:"success", stats} // terminal
|
|
107
|
+
* {type:"result", timestamp, status:"error", error:{type,message}, stats}
|
|
108
|
+
*
|
|
109
|
+
* Unlike Claude Code's `result` event, gemini's terminal event carries NO
|
|
110
|
+
* final assistant text — the reply must be assembled by concatenating every
|
|
111
|
+
* `message` event with `role:"assistant"` (the CLI emits them as deltas).
|
|
112
|
+
*
|
|
113
|
+
* ## systemContext
|
|
114
|
+
*
|
|
115
|
+
* Gemini's headless mode has no `--append-system-prompt` equivalent and
|
|
116
|
+
* `GEMINI_SYSTEM_MD` replaces the entire core system prompt (which would
|
|
117
|
+
* brick the agent — that core prompt scaffolds tool use). For v1 the
|
|
118
|
+
* adapter prepends `systemContext` directly to the positional prompt. Each
|
|
119
|
+
* turn re-injects the dynamic context so memory / digest updates take
|
|
120
|
+
* effect immediately; the trade-off is the resumed session transcript
|
|
121
|
+
* accumulates one prompt prefix per turn. Acceptable while we ship the
|
|
122
|
+
* connectivity layer — a follow-up can move systemContext into a
|
|
123
|
+
* daemon-managed `GEMINI.md` once we decide where to isolate it.
|
|
124
|
+
*
|
|
125
|
+
* ## Session continuity
|
|
126
|
+
*
|
|
127
|
+
* `gemini --session-id <uuid>` is for FRESH sessions only — it errors if
|
|
128
|
+
* the id already exists. `gemini --resume <uuid>` resolves the UUID against
|
|
129
|
+
* the project's existing session pool (gemini stores sessions per
|
|
130
|
+
* cwd-derived project hash, so the per-agent workspace already isolates
|
|
131
|
+
* them from the user's interactive sessions). We therefore:
|
|
132
|
+
* - new turn (sessionId=null): omit both flags; capture `init.session_id`.
|
|
133
|
+
* - continuation: pass `--resume <uuid>`. If gemini cannot resolve the id
|
|
134
|
+
* it exits with `FATAL_INPUT_ERROR` and stderr; we surface that as
|
|
135
|
+
* `errorText` and wipe `newSessionId` so the dispatcher discards the
|
|
136
|
+
* stale entry.
|
|
20
137
|
*/
|
|
21
|
-
export class GeminiAdapter {
|
|
138
|
+
export class GeminiAdapter extends NdjsonStreamAdapter {
|
|
22
139
|
id = "gemini";
|
|
140
|
+
explicitBinary;
|
|
141
|
+
resolvedBinary = null;
|
|
142
|
+
constructor(opts) {
|
|
143
|
+
super();
|
|
144
|
+
this.explicitBinary = opts?.binary ?? process.env.BOTCORD_GEMINI_BIN;
|
|
145
|
+
}
|
|
23
146
|
probe() {
|
|
24
147
|
return probeGemini();
|
|
25
148
|
}
|
|
26
|
-
async run(
|
|
27
|
-
|
|
149
|
+
async run(opts) {
|
|
150
|
+
if (opts.sessionId && !isValidGeminiSessionId(opts.sessionId)) {
|
|
151
|
+
throw new Error(invalidGeminiSessionIdError(opts.sessionId));
|
|
152
|
+
}
|
|
153
|
+
return super.run(opts);
|
|
154
|
+
}
|
|
155
|
+
resolveBinary() {
|
|
156
|
+
if (this.explicitBinary)
|
|
157
|
+
return this.explicitBinary;
|
|
158
|
+
if (this.resolvedBinary)
|
|
159
|
+
return this.resolvedBinary;
|
|
160
|
+
this.resolvedBinary = resolveGeminiCommand() ?? "gemini";
|
|
161
|
+
return this.resolvedBinary;
|
|
162
|
+
}
|
|
163
|
+
buildArgs(opts) {
|
|
164
|
+
const extraArgs = sanitizeGeminiExtraArgs(opts.extraArgs);
|
|
165
|
+
const args = [
|
|
166
|
+
"-p",
|
|
167
|
+
composePrompt(opts.text, opts.systemContext),
|
|
168
|
+
"--output-format",
|
|
169
|
+
"stream-json",
|
|
170
|
+
];
|
|
171
|
+
// Daemon-driven gemini turns are non-interactive. Auto-approve all tool
|
|
172
|
+
// use to avoid deadlocks; operators with stricter requirements can
|
|
173
|
+
// override via extraArgs `--approval-mode plan` etc.
|
|
174
|
+
if (!hasFlag(extraArgs, "--approval-mode") &&
|
|
175
|
+
!hasFlag(extraArgs, "-y") &&
|
|
176
|
+
!hasFlag(extraArgs, "--yolo")) {
|
|
177
|
+
args.push("--yolo");
|
|
178
|
+
}
|
|
179
|
+
// Trust the workspace so gemini doesn't downgrade the approval mode the
|
|
180
|
+
// moment cwd isn't in `~/.gemini/trustedFolders.json`. Without this the
|
|
181
|
+
// CLI silently flips back to "default" approval — which then deadlocks
|
|
182
|
+
// on tool calls because we have no prompt relay.
|
|
183
|
+
if (!hasFlag(extraArgs, "--skip-trust")) {
|
|
184
|
+
args.push("--skip-trust");
|
|
185
|
+
}
|
|
186
|
+
if (opts.sessionId) {
|
|
187
|
+
if (!isValidGeminiSessionId(opts.sessionId)) {
|
|
188
|
+
throw new Error(invalidGeminiSessionIdError(opts.sessionId));
|
|
189
|
+
}
|
|
190
|
+
args.push("--resume", opts.sessionId);
|
|
191
|
+
}
|
|
192
|
+
if (extraArgs.length)
|
|
193
|
+
args.push(...extraArgs);
|
|
194
|
+
return args;
|
|
195
|
+
}
|
|
196
|
+
spawnEnv(opts) {
|
|
197
|
+
return {
|
|
198
|
+
...super.spawnEnv(opts),
|
|
199
|
+
// Keep stream-json clean regardless of the user's terminal settings.
|
|
200
|
+
FORCE_COLOR: "0",
|
|
201
|
+
NO_COLOR: "1",
|
|
202
|
+
// Prevent gemini's launcher from re-spawning itself with --max-old-space
|
|
203
|
+
// tuning; the relaunch races with our stdio piping in tests and shaves
|
|
204
|
+
// ~200ms off every spawn in production.
|
|
205
|
+
GEMINI_CLI_NO_RELAUNCH: "1",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
handleEvent(raw, ctx) {
|
|
209
|
+
const obj = raw;
|
|
210
|
+
const status = geminiStatusEvent(obj);
|
|
211
|
+
if (status)
|
|
212
|
+
ctx.emitStatus(status);
|
|
213
|
+
ctx.emitBlock(normalizeBlock(obj, ctx.seq));
|
|
214
|
+
if (obj.type === "init" && typeof obj.session_id === "string") {
|
|
215
|
+
ctx.state.newSessionId = obj.session_id;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (obj.type === "message" && obj.role === "assistant" && typeof obj.content === "string") {
|
|
219
|
+
ctx.appendAssistantText(obj.content);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (obj.type === "result") {
|
|
223
|
+
if (obj.status === "error") {
|
|
224
|
+
const errMsg = obj.error?.message;
|
|
225
|
+
ctx.state.errorText =
|
|
226
|
+
typeof errMsg === "string" && errMsg ? errMsg : "gemini run failed";
|
|
227
|
+
// Drop the captured session id so the dispatcher doesn't try to
|
|
228
|
+
// resume a session that may not have been persisted to disk.
|
|
229
|
+
ctx.state.newSessionId = "";
|
|
230
|
+
ctx.state.finalText = "";
|
|
231
|
+
ctx.state.assistantTextChunks = [];
|
|
232
|
+
ctx.state.assistantTextBytes = 0;
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (obj.type === "error" && obj.severity === "error" && typeof obj.message === "string") {
|
|
237
|
+
// Severity "error" is fatal in gemini's classification; "warning" is
|
|
238
|
+
// recoverable and shouldn't override the assistant's output.
|
|
239
|
+
ctx.state.errorText = obj.message;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Prepend systemContext to the user prompt. Empty systemContext (the common
|
|
245
|
+
* case for direct DMs) returns the prompt unchanged so we don't bloat the
|
|
246
|
+
* token count with a marker prefix that conveys nothing.
|
|
247
|
+
*/
|
|
248
|
+
function composePrompt(text, systemContext) {
|
|
249
|
+
if (!systemContext || !systemContext.trim())
|
|
250
|
+
return text;
|
|
251
|
+
return `${systemContext.trim()}\n\n---\n\n${text}`;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Map a gemini stream-json event to a `RuntimeStatusEvent`. Only the
|
|
255
|
+
* lifecycle transitions the dispatcher can't infer from `StreamBlock.kind`
|
|
256
|
+
* land here; everything else is left to auto-synthesis.
|
|
257
|
+
*/
|
|
258
|
+
function geminiStatusEvent(obj) {
|
|
259
|
+
if (obj.type === "init") {
|
|
260
|
+
return { kind: "thinking", phase: "started", label: "Starting session" };
|
|
261
|
+
}
|
|
262
|
+
if (obj.type === "tool_use") {
|
|
263
|
+
const name = typeof obj.tool_name === "string" && obj.tool_name ? obj.tool_name : "tool";
|
|
264
|
+
return { kind: "thinking", phase: "updated", label: name };
|
|
265
|
+
}
|
|
266
|
+
if (obj.type === "message" && obj.role === "assistant") {
|
|
267
|
+
return { kind: "thinking", phase: "stopped" };
|
|
268
|
+
}
|
|
269
|
+
if (obj.type === "result") {
|
|
270
|
+
return { kind: "thinking", phase: "stopped" };
|
|
271
|
+
}
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
function normalizeBlock(obj, seq) {
|
|
275
|
+
let kind = "other";
|
|
276
|
+
const type = obj?.type;
|
|
277
|
+
if (type === "message" && obj?.role === "assistant") {
|
|
278
|
+
kind = "assistant_text";
|
|
279
|
+
}
|
|
280
|
+
else if (type === "tool_use") {
|
|
281
|
+
kind = "tool_use";
|
|
282
|
+
}
|
|
283
|
+
else if (type === "tool_result") {
|
|
284
|
+
kind = "tool_result";
|
|
285
|
+
}
|
|
286
|
+
else if (type === "init" || type === "result") {
|
|
287
|
+
kind = "system";
|
|
28
288
|
}
|
|
289
|
+
return { raw: obj, kind, seq };
|
|
29
290
|
}
|
|
@@ -39,7 +39,7 @@ export declare const deepseekTuiModule: RuntimeModule;
|
|
|
39
39
|
export declare const kimiModule: RuntimeModule;
|
|
40
40
|
/** Built-in runtime module entry for Hermes Agent (ACP stdio). */
|
|
41
41
|
export declare const hermesAgentModule: RuntimeModule;
|
|
42
|
-
/** Built-in runtime module entry for Gemini
|
|
42
|
+
/** Built-in runtime module entry for Gemini CLI. */
|
|
43
43
|
export declare const geminiModule: RuntimeModule;
|
|
44
44
|
/** Built-in runtime module entry for OpenClaw (ACP). */
|
|
45
45
|
export declare const openclawAcpModule: RuntimeModule;
|
|
@@ -51,14 +51,15 @@ export const hermesAgentModule = {
|
|
|
51
51
|
create: () => new HermesAgentAdapter(),
|
|
52
52
|
installHint: 'Install: pip install "hermes-agent[acp]" (or set BOTCORD_HERMES_AGENT_BIN to the absolute path of hermes-acp)',
|
|
53
53
|
};
|
|
54
|
-
/** Built-in runtime module entry for Gemini
|
|
54
|
+
/** Built-in runtime module entry for Gemini CLI. */
|
|
55
55
|
export const geminiModule = {
|
|
56
56
|
id: "gemini",
|
|
57
57
|
displayName: "Gemini CLI",
|
|
58
58
|
binary: "gemini",
|
|
59
|
+
envVar: "BOTCORD_GEMINI_BIN",
|
|
59
60
|
probe: () => probeGemini(),
|
|
60
61
|
create: () => new GeminiAdapter(),
|
|
61
|
-
|
|
62
|
+
installHint: "Install with `npm install -g @google/gemini-cli` (or `brew install gemini-cli`) and run `gemini` once to complete authentication. Override the binary with BOTCORD_GEMINI_BIN.",
|
|
62
63
|
};
|
|
63
64
|
/** Built-in runtime module entry for OpenClaw (ACP). */
|
|
64
65
|
export const openclawAcpModule = {
|
package/dist/provision.d.ts
CHANGED
package/dist/provision.js
CHANGED
|
@@ -20,12 +20,25 @@ import { discoverAgentCredentials } from "./agent-discovery.js";
|
|
|
20
20
|
import { resolveMemoryDir } from "./working-memory.js";
|
|
21
21
|
import { discoverRuntimeModelCatalog } from "./runtime-models.js";
|
|
22
22
|
import { collectAgentSkillSnapshot } from "./skill-index.js";
|
|
23
|
+
import { installAgentSkillManifest, installBotLearnArchiveManifest, installVercelSkillsForAgent, } from "./skill-installer.js";
|
|
23
24
|
import { buildRuntimeSelectionExtraArgs, mergeRuntimeExtraArgs, } from "./runtime-route-options.js";
|
|
24
25
|
import { handleCloudGatewayRuntimeInbound, } from "./cloud-gateway-runtime.js";
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
.find((
|
|
28
|
-
|
|
26
|
+
function skillIndexOptionsForLoadedAgent(gateway, agentId) {
|
|
27
|
+
const route = gateway.listManagedRoutes()
|
|
28
|
+
.find((entry) => entry.match?.accountId === agentId);
|
|
29
|
+
let credentials = null;
|
|
30
|
+
try {
|
|
31
|
+
credentials = loadStoredCredentials(defaultCredentialsFile(agentId));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
credentials = null;
|
|
35
|
+
}
|
|
36
|
+
const runtime = route?.runtime ?? credentials?.runtime;
|
|
37
|
+
const hermesProfile = route?.hermesProfile ?? credentials?.hermesProfile;
|
|
38
|
+
return {
|
|
39
|
+
...(runtime ? { runtime } : {}),
|
|
40
|
+
...(hermesProfile ? { hermesProfile } : {}),
|
|
41
|
+
};
|
|
29
42
|
}
|
|
30
43
|
/**
|
|
31
44
|
* Build a dispatcher function that routes a `ControlFrame` to the right
|
|
@@ -339,15 +352,76 @@ export function createProvisioner(opts) {
|
|
|
339
352
|
},
|
|
340
353
|
};
|
|
341
354
|
}
|
|
342
|
-
const
|
|
343
|
-
const result = collectAgentSkillSnapshot(params.agentId,
|
|
355
|
+
const skillIndexOptions = skillIndexOptionsForLoadedAgent(gateway, params.agentId);
|
|
356
|
+
const result = collectAgentSkillSnapshot(params.agentId, skillIndexOptions);
|
|
344
357
|
daemonLog.debug("list_agent_skills", {
|
|
345
358
|
agentId: params.agentId,
|
|
346
|
-
runtime,
|
|
359
|
+
runtime: skillIndexOptions.runtime,
|
|
360
|
+
hermesProfile: skillIndexOptions.hermesProfile ?? null,
|
|
347
361
|
count: result.skills.length,
|
|
348
362
|
});
|
|
349
363
|
return { ok: true, result };
|
|
350
364
|
}
|
|
365
|
+
case CONTROL_FRAME_TYPES.INSTALL_AGENT_SKILL: {
|
|
366
|
+
const params = (frame.params ?? {});
|
|
367
|
+
if (!params.agentId) {
|
|
368
|
+
return {
|
|
369
|
+
ok: false,
|
|
370
|
+
error: { code: "bad_params", message: "install_agent_skill requires params.agentId" },
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const channels = gateway.snapshot().channels;
|
|
374
|
+
if (!channels[params.agentId]) {
|
|
375
|
+
return {
|
|
376
|
+
ok: false,
|
|
377
|
+
error: {
|
|
378
|
+
code: "agent_not_loaded",
|
|
379
|
+
message: `agent ${params.agentId} is not loaded in daemon gateway`,
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
const skillIndexOptions = skillIndexOptionsForLoadedAgent(gateway, params.agentId);
|
|
384
|
+
const runtime = skillIndexOptions.runtime;
|
|
385
|
+
const modes = [params.manifest, params.archiveManifest, params.vercel].filter(Boolean).length;
|
|
386
|
+
if (modes !== 1) {
|
|
387
|
+
return {
|
|
388
|
+
ok: false,
|
|
389
|
+
error: {
|
|
390
|
+
code: "bad_params",
|
|
391
|
+
message: "install_agent_skill requires exactly one of manifest, archiveManifest, or vercel",
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
let result;
|
|
396
|
+
try {
|
|
397
|
+
result = params.vercel
|
|
398
|
+
? await installVercelSkillsForAgent({
|
|
399
|
+
agentId: params.agentId,
|
|
400
|
+
packageSpec: params.vercel.packageSpec,
|
|
401
|
+
skills: params.vercel.skills,
|
|
402
|
+
runtime,
|
|
403
|
+
})
|
|
404
|
+
: params.archiveManifest
|
|
405
|
+
? installBotLearnArchiveManifest(params.agentId, params.archiveManifest, { runtime })
|
|
406
|
+
: installAgentSkillManifest(params.agentId, params.manifest, { runtime });
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
return {
|
|
410
|
+
ok: false,
|
|
411
|
+
error: {
|
|
412
|
+
code: "skill_install_failed",
|
|
413
|
+
message: err instanceof Error ? err.message : String(err),
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
daemonLog.debug("install_agent_skill", {
|
|
418
|
+
agentId: params.agentId,
|
|
419
|
+
runtime,
|
|
420
|
+
installed: result.installed.map((s) => s.name),
|
|
421
|
+
snapshotCount: result.snapshot.skills.length,
|
|
422
|
+
});
|
|
423
|
+
return { ok: true, result };
|
|
424
|
+
}
|
|
351
425
|
case "wake_agent": {
|
|
352
426
|
return handleWakeAgent(gateway, frame.params);
|
|
353
427
|
}
|
|
@@ -823,6 +897,7 @@ async function installLocalAgent(credentials, ctx) {
|
|
|
823
897
|
hubUrl: credentials.hubUrl,
|
|
824
898
|
...(credentials.displayName ? { displayName: credentials.displayName } : {}),
|
|
825
899
|
...(credentials.runtime ? { runtime: credentials.runtime } : {}),
|
|
900
|
+
...(credentials.hermesProfile ? { hermesProfile: credentials.hermesProfile } : {}),
|
|
826
901
|
});
|
|
827
902
|
}
|
|
828
903
|
catch (err) {
|
|
@@ -904,6 +979,7 @@ async function installExistingOpenclawBinding(agentId, ctx) {
|
|
|
904
979
|
hubUrl: credentials.hubUrl,
|
|
905
980
|
...(credentials.displayName ? { displayName: credentials.displayName } : {}),
|
|
906
981
|
...(credentials.runtime ? { runtime: credentials.runtime } : {}),
|
|
982
|
+
...(credentials.hermesProfile ? { hermesProfile: credentials.hermesProfile } : {}),
|
|
907
983
|
});
|
|
908
984
|
}
|
|
909
985
|
catch (err) {
|