@anna-ai/cli 0.1.12 → 0.1.16
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/agent-DUmINbo4.js +372 -0
- package/dist/{apps-CDe6Fjq2.js → apps-Bt9CT5Sl.js} +1 -1
- package/dist/bridge-AJilXBw2.js +3 -0
- package/dist/{bridge-BQUo6ehX.js → bridge-nqQ3-j-t.js} +1 -1
- package/dist/cli.js +78 -9
- package/dist/dev-BRlFgo2I.js +3 -0
- package/dist/{dev-DoY58pBM.js → dev-C6v5yRV2.js} +4 -4
- package/dist/dev-account-DCyjamBa.js +44 -0
- package/dist/dev-app-cache-DAHcq46m.js +175 -0
- package/dist/dev-app-cache-DGF2Kuzd.js +4 -0
- package/dist/{doctor-DP2UB10l.js → doctor-C8MWfLt8.js} +1 -1
- package/dist/executa-dev-4FZ7AJHR.js +259 -0
- package/dist/executa-init-COEmKDOE.js +68 -0
- package/dist/executa-register-66WKIwQQ.js +47 -0
- package/dist/host_upload-C_pGOS6p.js +136 -0
- package/dist/image-bwolX7pa.js +131 -0
- package/dist/mascot-wlYTJqMs.js +218 -0
- package/dist/runner-DmGLdat0.js +322 -0
- package/dist/sampling-CJUDG-mf.js +155 -0
- package/dist/storage-EQJA_0UW.js +316 -0
- package/package.json +1 -1
- package/templates/executa/go/README.md +10 -0
- package/templates/executa/go/executa.json +4 -0
- package/templates/executa/go/go.mod +3 -0
- package/templates/executa/go/main.go +148 -0
- package/templates/executa/node/README.md +12 -0
- package/templates/executa/node/executa.json +4 -0
- package/templates/executa/node/package.json +12 -0
- package/templates/executa/node/plugin.mjs +126 -0
- package/templates/executa/node/sampling-fixture.jsonl +1 -0
- package/templates/executa/python/README.md +23 -0
- package/templates/executa/python/__SLUG_PY___plugin.py +146 -0
- package/templates/executa/python/executa.json +4 -0
- package/templates/executa/python/pyproject.toml +15 -0
- package/templates/executa/python/sampling-fixture.jsonl +4 -0
- package/templates/minimal/bundle/app.js +2 -0
- package/templates/minimal/bundle/index.html +1 -2
- package/dist/bridge-BEHyfpPI.js +0 -3
- package/dist/dev-app-cache-BMfOlTHd.js +0 -93
- package/dist/dev-app-cache-cXvO2XwQ.js +0 -4
- /package/dist/{credentials-CIOYq2Lm.js → credentials-DDqx6XMQ.js} +0 -0
- /package/dist/{fixture-BEu4LXLG.js → fixture-CATHyLLI.js} +0 -0
- /package/dist/{login-dl1Zfny8.js → login-CsIVbrmf.js} +0 -0
- /package/dist/{logout-DablvlFs.js → logout-gfmKQxMj.js} +0 -0
- /package/dist/{server-NXmiWJjX.js → server-D8R6ppOp.js} +0 -0
- /package/dist/{whoami-giXOY415.js → whoami-BS5wy-Nh.js} +0 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
|
|
4
|
+
//#region src/executa/runner.ts
|
|
5
|
+
/** Spec describing where the host advertises its capabilities. */
|
|
6
|
+
const HOST_INITIALIZE_PARAMS = {
|
|
7
|
+
protocolVersion: "2.0",
|
|
8
|
+
client_capabilities: {
|
|
9
|
+
sampling: {},
|
|
10
|
+
agent: {},
|
|
11
|
+
storage: {},
|
|
12
|
+
image: {},
|
|
13
|
+
"image.edit": {},
|
|
14
|
+
upload: {}
|
|
15
|
+
},
|
|
16
|
+
client_info: {
|
|
17
|
+
name: "anna-app-cli",
|
|
18
|
+
version: "executa-dev"
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var ExecutaRunner = class {
|
|
22
|
+
proc = null;
|
|
23
|
+
nextId = 1;
|
|
24
|
+
pending = new Map();
|
|
25
|
+
exitWaiters = [];
|
|
26
|
+
closed = false;
|
|
27
|
+
/** Resolved by start() after the v2 handshake (or v1 fallback) succeeds. */
|
|
28
|
+
initialized = null;
|
|
29
|
+
constructor(opts) {
|
|
30
|
+
this.opts = opts;
|
|
31
|
+
}
|
|
32
|
+
/** Spawn the subprocess and perform the initialize handshake. */
|
|
33
|
+
async start() {
|
|
34
|
+
if (this.proc) {
|
|
35
|
+
if (!this.initialized) throw new Error("executa already starting");
|
|
36
|
+
return this.initialized;
|
|
37
|
+
}
|
|
38
|
+
const [bin, ...args] = this.opts.command;
|
|
39
|
+
if (!bin) throw new Error("ExecutaRunner: empty command");
|
|
40
|
+
const env = {
|
|
41
|
+
...process.env,
|
|
42
|
+
...this.opts.env
|
|
43
|
+
};
|
|
44
|
+
env.ANNA_EXECUTA_DEV = "1";
|
|
45
|
+
this.proc = spawn(bin, args, {
|
|
46
|
+
cwd: this.opts.cwd,
|
|
47
|
+
stdio: [
|
|
48
|
+
"pipe",
|
|
49
|
+
"pipe",
|
|
50
|
+
"pipe"
|
|
51
|
+
],
|
|
52
|
+
env
|
|
53
|
+
});
|
|
54
|
+
const stderrSink = this.opts.onStderr ?? ((l) => process.stderr.write(`[executa] ${l}\n`));
|
|
55
|
+
createInterface({ input: this.proc.stderr }).on("line", stderrSink);
|
|
56
|
+
this.proc.on("exit", (code) => {
|
|
57
|
+
this.closed = true;
|
|
58
|
+
const waiters = this.exitWaiters.splice(0);
|
|
59
|
+
for (const w of waiters) w(code);
|
|
60
|
+
const err = new Error(`executa exited (code=${code ?? "null"})`);
|
|
61
|
+
for (const p of this.pending.values()) p.reject(err);
|
|
62
|
+
this.pending.clear();
|
|
63
|
+
});
|
|
64
|
+
this.proc.on("error", (e) => {
|
|
65
|
+
this.closed = true;
|
|
66
|
+
for (const p of this.pending.values()) p.reject(e);
|
|
67
|
+
this.pending.clear();
|
|
68
|
+
});
|
|
69
|
+
createInterface({ input: this.proc.stdout }).on("line", (line) => {
|
|
70
|
+
this.handleLine(line, stderrSink);
|
|
71
|
+
});
|
|
72
|
+
const initTimeout = this.opts.initTimeoutMs ?? 8e3;
|
|
73
|
+
try {
|
|
74
|
+
const raw = await this.callWithTimeout("initialize", HOST_INITIALIZE_PARAMS, initTimeout);
|
|
75
|
+
const ver = String(raw.protocolVersion ?? "1.0");
|
|
76
|
+
this.initialized = {
|
|
77
|
+
protocolVersion: ver === "2.0" ? "2.0" : "1.0",
|
|
78
|
+
raw
|
|
79
|
+
};
|
|
80
|
+
} catch (e) {
|
|
81
|
+
const rpc = e.rpc;
|
|
82
|
+
if (rpc && rpc.code === -32601) this.initialized = {
|
|
83
|
+
protocolVersion: "1.0",
|
|
84
|
+
raw: {}
|
|
85
|
+
};
|
|
86
|
+
else {
|
|
87
|
+
await this.stop();
|
|
88
|
+
throw e;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return this.initialized;
|
|
92
|
+
}
|
|
93
|
+
/** Send a JSON-RPC request and await the response. */
|
|
94
|
+
call(method, params = {}) {
|
|
95
|
+
if (this.closed || !this.proc) return Promise.reject(new Error("executa not running"));
|
|
96
|
+
const id = this.nextId++;
|
|
97
|
+
const env = {
|
|
98
|
+
jsonrpc: "2.0",
|
|
99
|
+
id,
|
|
100
|
+
method,
|
|
101
|
+
params
|
|
102
|
+
};
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
this.pending.set(id, {
|
|
105
|
+
resolve: (v) => resolve(v),
|
|
106
|
+
reject
|
|
107
|
+
});
|
|
108
|
+
this.proc.stdin.write(`${JSON.stringify(env)}\n`);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/** Convenience: `describe → MANIFEST`. */
|
|
112
|
+
describe() {
|
|
113
|
+
return this.call("describe");
|
|
114
|
+
}
|
|
115
|
+
/** Convenience: `invoke {tool, arguments}`. */
|
|
116
|
+
invoke(tool, args) {
|
|
117
|
+
return this.call("invoke", {
|
|
118
|
+
tool,
|
|
119
|
+
arguments: args
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/** Convenience: `health → {status}`. */
|
|
123
|
+
health() {
|
|
124
|
+
return this.call("health");
|
|
125
|
+
}
|
|
126
|
+
/** Close stdin (graceful EOF shutdown) then SIGTERM after a grace period. */
|
|
127
|
+
async stop(graceMs = 1500) {
|
|
128
|
+
if (!this.proc) return;
|
|
129
|
+
this.closed = true;
|
|
130
|
+
const p = this.proc;
|
|
131
|
+
try {
|
|
132
|
+
p.stdin.end();
|
|
133
|
+
} catch {}
|
|
134
|
+
const waitExit = new Promise((resolve) => {
|
|
135
|
+
this.exitWaiters.push(resolve);
|
|
136
|
+
if (p.exitCode != null) resolve(p.exitCode);
|
|
137
|
+
});
|
|
138
|
+
const timer = new Promise((resolve) => {
|
|
139
|
+
setTimeout(() => resolve("timeout"), graceMs);
|
|
140
|
+
});
|
|
141
|
+
const winner = await Promise.race([waitExit, timer]);
|
|
142
|
+
if (winner === "timeout") try {
|
|
143
|
+
p.kill("SIGTERM");
|
|
144
|
+
} catch {}
|
|
145
|
+
this.proc = null;
|
|
146
|
+
}
|
|
147
|
+
async callWithTimeout(method, params, ms) {
|
|
148
|
+
const id = this.nextId++;
|
|
149
|
+
const env = {
|
|
150
|
+
jsonrpc: "2.0",
|
|
151
|
+
id,
|
|
152
|
+
method,
|
|
153
|
+
params
|
|
154
|
+
};
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
const to = setTimeout(() => {
|
|
157
|
+
this.pending.delete(id);
|
|
158
|
+
reject(new Error(`executa ${method} timed out after ${ms}ms`));
|
|
159
|
+
}, ms);
|
|
160
|
+
this.pending.set(id, {
|
|
161
|
+
resolve: (v) => {
|
|
162
|
+
clearTimeout(to);
|
|
163
|
+
resolve(v);
|
|
164
|
+
},
|
|
165
|
+
reject: (e) => {
|
|
166
|
+
clearTimeout(to);
|
|
167
|
+
reject(e);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
this.proc.stdin.write(`${JSON.stringify(env)}\n`);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
handleLine(line, stderrSink) {
|
|
174
|
+
if (!line.trim()) return;
|
|
175
|
+
let env;
|
|
176
|
+
try {
|
|
177
|
+
env = JSON.parse(line);
|
|
178
|
+
} catch {
|
|
179
|
+
stderrSink(`non-json from executa stdout: ${line}`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (typeof env.method === "string") {
|
|
183
|
+
this.handleReverse(env);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const id = env.id;
|
|
187
|
+
if (typeof id !== "number") return;
|
|
188
|
+
const pending = this.pending.get(id);
|
|
189
|
+
if (!pending) return;
|
|
190
|
+
this.pending.delete(id);
|
|
191
|
+
if (env.error) {
|
|
192
|
+
const e = env.error;
|
|
193
|
+
const err = new Error(`[${e.code}] ${e.message}`);
|
|
194
|
+
err.rpc = e;
|
|
195
|
+
pending.reject(err);
|
|
196
|
+
} else pending.resolve(env.result);
|
|
197
|
+
}
|
|
198
|
+
async handleReverse(env) {
|
|
199
|
+
const id = env.id;
|
|
200
|
+
const method = env.method;
|
|
201
|
+
const params = env.params ?? {};
|
|
202
|
+
const respond = (body) => {
|
|
203
|
+
if (id === void 0 || id === null) return;
|
|
204
|
+
if (!this.proc) return;
|
|
205
|
+
const out = {
|
|
206
|
+
jsonrpc: "2.0",
|
|
207
|
+
id,
|
|
208
|
+
...body
|
|
209
|
+
};
|
|
210
|
+
try {
|
|
211
|
+
this.proc.stdin.write(`${JSON.stringify(out)}\n`);
|
|
212
|
+
} catch {}
|
|
213
|
+
};
|
|
214
|
+
if (method === "sampling/createMessage") {
|
|
215
|
+
if (!this.opts.sampling) {
|
|
216
|
+
respond({ error: {
|
|
217
|
+
code: -32008,
|
|
218
|
+
message: "sampling not configured (use `anna-app login` and rerun without `--no-sampling`, or pass `--mock-sampling`)"
|
|
219
|
+
} });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
const result = await this.opts.sampling.createMessage(params);
|
|
224
|
+
respond({ result });
|
|
225
|
+
} catch (e) {
|
|
226
|
+
const code = e.rpcCode ?? -32e3;
|
|
227
|
+
respond({ error: {
|
|
228
|
+
code,
|
|
229
|
+
message: e.message
|
|
230
|
+
} });
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (method.startsWith("agent/")) {
|
|
235
|
+
if (!this.opts.agent) {
|
|
236
|
+
respond({ error: {
|
|
237
|
+
code: -32041,
|
|
238
|
+
message: "agent reverse-RPC not configured (rerun without `--no-agent`, or pass `--mock-agent <fixture>`)"
|
|
239
|
+
} });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
const result = await this.opts.agent.call(method, params);
|
|
244
|
+
respond({ result });
|
|
245
|
+
} catch (e) {
|
|
246
|
+
const code = e.rpcCode ?? -32e3;
|
|
247
|
+
respond({ error: {
|
|
248
|
+
code,
|
|
249
|
+
message: e.message
|
|
250
|
+
} });
|
|
251
|
+
}
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (method === "image/generate" || method === "image/edit") {
|
|
255
|
+
if (!this.opts.image) {
|
|
256
|
+
respond({ error: {
|
|
257
|
+
code: -32101,
|
|
258
|
+
message: "image not configured (rerun without `--no-image`, or pass `--mock-image <fixture>` / `--app-slug`)"
|
|
259
|
+
} });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
const result = method === "image/generate" ? await this.opts.image.generate(params) : await this.opts.image.edit(params);
|
|
264
|
+
respond({ result });
|
|
265
|
+
} catch (e) {
|
|
266
|
+
const code = e.rpcCode ?? -32103;
|
|
267
|
+
respond({ error: {
|
|
268
|
+
code,
|
|
269
|
+
message: e.message
|
|
270
|
+
} });
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (method === "host/uploadFile") {
|
|
275
|
+
if (!this.opts.hostUpload) {
|
|
276
|
+
respond({ error: {
|
|
277
|
+
code: -32201,
|
|
278
|
+
message: "host upload not configured (rerun without `--no-upload`, or pass `--mock-upload <fixture>` / `--app-slug`)"
|
|
279
|
+
} });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
const result = await this.opts.hostUpload.call(method, params);
|
|
284
|
+
respond({ result });
|
|
285
|
+
} catch (e) {
|
|
286
|
+
const code = e.rpcCode ?? -32207;
|
|
287
|
+
respond({ error: {
|
|
288
|
+
code,
|
|
289
|
+
message: e.message
|
|
290
|
+
} });
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (method.startsWith("storage/") || method.startsWith("files/")) {
|
|
295
|
+
if (!this.opts.storage) {
|
|
296
|
+
respond({ error: {
|
|
297
|
+
code: -32021,
|
|
298
|
+
message: "storage not configured (rerun with `--storage memory|mock|real`)"
|
|
299
|
+
} });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
const result = await this.opts.storage.call(method, params);
|
|
304
|
+
respond({ result });
|
|
305
|
+
} catch (e) {
|
|
306
|
+
const code = e.rpcCode ?? -32e3;
|
|
307
|
+
respond({ error: {
|
|
308
|
+
code,
|
|
309
|
+
message: e.message
|
|
310
|
+
} });
|
|
311
|
+
}
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
respond({ error: {
|
|
315
|
+
code: -32601,
|
|
316
|
+
message: `unknown reverse method: ${method}`
|
|
317
|
+
} });
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
//#endregion
|
|
322
|
+
export { ExecutaRunner };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { canonicalHost, getAccount } from "./credentials-BTv2IfUZ.js";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
//#region src/executa/sampling.ts
|
|
6
|
+
var SamplingBridge = class {
|
|
7
|
+
mocks = [];
|
|
8
|
+
cachedSession = null;
|
|
9
|
+
constructor(opts) {
|
|
10
|
+
this.opts = opts;
|
|
11
|
+
if (opts.mode === "mock" && opts.mockFile) {
|
|
12
|
+
const path = resolve(opts.mockFile);
|
|
13
|
+
if (existsSync(path)) for (const line of readFileSync(path, "utf8").split(/\r?\n/)) {
|
|
14
|
+
if (!line.trim() || line.startsWith("#")) continue;
|
|
15
|
+
try {
|
|
16
|
+
this.mocks.push(JSON.parse(line));
|
|
17
|
+
} catch {}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async createMessage(req) {
|
|
22
|
+
if (this.opts.mode === "off") throw withCode(new Error("sampling disabled — rerun without `--no-sampling`"), -32008);
|
|
23
|
+
if (this.opts.mode === "mock") return this.mockMessage(req);
|
|
24
|
+
return this.realMessage(req);
|
|
25
|
+
}
|
|
26
|
+
mockMessage(req) {
|
|
27
|
+
const content = collectContent(req);
|
|
28
|
+
const entry = this.mocks.find((m) => m.ns === "sampling" && m.method === "createMessage" || m.ns === "llm" && m.method === "complete") ?? null;
|
|
29
|
+
const matched = entry && (!entry.match?.contentIncludes || content.includes(entry.match.contentIncludes)) ? entry : this.mocks.find((m) => m.ns === "sampling" && m.method === "createMessage" || m.ns === "llm" && m.method === "complete");
|
|
30
|
+
if (matched && matched.result) return normaliseSamplingResult(matched.result, "mock-model");
|
|
31
|
+
return {
|
|
32
|
+
role: "assistant",
|
|
33
|
+
content: {
|
|
34
|
+
type: "text",
|
|
35
|
+
text: "(mock) no fixture matched"
|
|
36
|
+
},
|
|
37
|
+
model: "mock-model",
|
|
38
|
+
stopReason: "endTurn"
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
account() {
|
|
42
|
+
const acc = getAccount(this.opts.account);
|
|
43
|
+
if (!acc) throw withCode(new Error("no PAT on disk — run `anna-app login --host <nexus-url>` first (or pass `--mock-sampling <fixture>` / `--no-sampling`)"), -32001);
|
|
44
|
+
if (acc.expires_at && acc.expires_at < Math.floor(Date.now() / 1e3)) throw withCode(new Error("PAT expired — run `anna-app login` again"), -32001);
|
|
45
|
+
return acc;
|
|
46
|
+
}
|
|
47
|
+
async mint() {
|
|
48
|
+
if (this.cachedSession && this.cachedSession.expiresAt - 30 > Math.floor(Date.now() / 1e3)) return this.cachedSession;
|
|
49
|
+
const acc = this.account();
|
|
50
|
+
if (!this.opts.appSlug) throw withCode(new Error("sampling bridge has no app_slug — pass `--app-slug <slug>` to `anna-app executa dev` so nexus can attribute the spend"), -32602);
|
|
51
|
+
const url = `${canonicalHost(acc.host)}/api/v1/anna-apps/dev/session/mint`;
|
|
52
|
+
const res = await fetch(url, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: { "content-type": "application/json" },
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
pat: acc.pat,
|
|
57
|
+
kind: "complete",
|
|
58
|
+
app_slug: this.opts.appSlug
|
|
59
|
+
})
|
|
60
|
+
});
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
const text = await res.text().catch(() => "");
|
|
63
|
+
throw withCode(new Error(`session.mint failed: HTTP ${res.status} ${text}`), -32e3);
|
|
64
|
+
}
|
|
65
|
+
const body = await res.json();
|
|
66
|
+
this.cachedSession = {
|
|
67
|
+
appSessionToken: body.app_session_token,
|
|
68
|
+
expiresAt: Math.floor(Date.now() / 1e3) + (body.expires_in || 600)
|
|
69
|
+
};
|
|
70
|
+
return this.cachedSession;
|
|
71
|
+
}
|
|
72
|
+
async realMessage(req) {
|
|
73
|
+
const acc = this.account();
|
|
74
|
+
const session = await this.mint();
|
|
75
|
+
const body = mcpToCompleteBody(req);
|
|
76
|
+
const res = await fetch(`${canonicalHost(acc.host)}/api/v1/copilot/app/complete`, {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: {
|
|
79
|
+
"content-type": "application/json",
|
|
80
|
+
authorization: `Bearer ${session.appSessionToken}`
|
|
81
|
+
},
|
|
82
|
+
body: JSON.stringify(body)
|
|
83
|
+
});
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
const text = await res.text().catch(() => "");
|
|
86
|
+
throw withCode(new Error(`HTTP ${res.status}: ${text}`), -32e3);
|
|
87
|
+
}
|
|
88
|
+
const out = await res.json();
|
|
89
|
+
return normaliseSamplingResult(out, out.model ?? "unknown");
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
function withCode(err, code) {
|
|
93
|
+
err.rpcCode = code;
|
|
94
|
+
return err;
|
|
95
|
+
}
|
|
96
|
+
/** Collect a single string view of a sampling request — used for mock matching. */
|
|
97
|
+
function collectContent(req) {
|
|
98
|
+
const parts = [];
|
|
99
|
+
if (req.systemPrompt) parts.push(req.systemPrompt);
|
|
100
|
+
for (const m of req.messages ?? []) {
|
|
101
|
+
const c = m.content;
|
|
102
|
+
if (typeof c === "string") parts.push(c);
|
|
103
|
+
else if (c && typeof c === "object" && "text" in c) parts.push(c.text);
|
|
104
|
+
}
|
|
105
|
+
return parts.join("\n");
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Translate an MCP `sampling/createMessage` request to the nexus
|
|
109
|
+
* `/api/v1/copilot/app/complete` body. Mapping is intentionally lossy —
|
|
110
|
+
* we forward only the fields nexus understands.
|
|
111
|
+
*/
|
|
112
|
+
function mcpToCompleteBody(req) {
|
|
113
|
+
const messages = (req.messages ?? []).map((m) => ({
|
|
114
|
+
role: m.role,
|
|
115
|
+
content: typeof m.content === "string" ? m.content : m.content.text
|
|
116
|
+
}));
|
|
117
|
+
const out = { messages };
|
|
118
|
+
if (req.systemPrompt) out.system = req.systemPrompt;
|
|
119
|
+
if (req.maxTokens != null) out.max_tokens = req.maxTokens;
|
|
120
|
+
if (req.temperature != null) out.temperature = req.temperature;
|
|
121
|
+
if (req.stopSequences) out.stop = req.stopSequences;
|
|
122
|
+
const hint = req.modelPreferences?.hints?.[0]?.name;
|
|
123
|
+
if (hint) out.model_hint = hint;
|
|
124
|
+
if (req.metadata) out.metadata = req.metadata;
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
/** Coerce arbitrary nexus / fixture output into a `SamplingResult`. */
|
|
128
|
+
function normaliseSamplingResult(raw, fallbackModel) {
|
|
129
|
+
if (!raw || typeof raw !== "object") return {
|
|
130
|
+
role: "assistant",
|
|
131
|
+
content: {
|
|
132
|
+
type: "text",
|
|
133
|
+
text: String(raw ?? "")
|
|
134
|
+
},
|
|
135
|
+
model: fallbackModel,
|
|
136
|
+
stopReason: "endTurn"
|
|
137
|
+
};
|
|
138
|
+
const r = raw;
|
|
139
|
+
if (r.role === "assistant" && r.content && typeof r.content === "object") return r;
|
|
140
|
+
const text = typeof r.text === "string" && r.text || (typeof r.content === "string" ? r.content : "") || (typeof r.message === "string" ? r.message : "") || "";
|
|
141
|
+
return {
|
|
142
|
+
role: "assistant",
|
|
143
|
+
content: {
|
|
144
|
+
type: "text",
|
|
145
|
+
text
|
|
146
|
+
},
|
|
147
|
+
model: r.model ?? fallbackModel,
|
|
148
|
+
stopReason: r.stop_reason ?? r.stopReason ?? "endTurn",
|
|
149
|
+
usage: r.usage ?? void 0,
|
|
150
|
+
_meta: { source: "nexus" }
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
//#endregion
|
|
155
|
+
export { SamplingBridge };
|