@octocodeai/freellama-mcp-server 0.5.0 → 0.6.1

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/index.js CHANGED
@@ -3837,7 +3837,7 @@ var require_fast_uri = __commonJS({
3837
3837
  normalizeString(uri, options);
3838
3838
  } else if (typeof uri === "object") {
3839
3839
  uri = /** @type {T} */
3840
- parse3(serialize2(uri, options), options);
3840
+ parse3(serialize(uri, options), options);
3841
3841
  }
3842
3842
  return uri;
3843
3843
  }
@@ -3872,13 +3872,13 @@ var require_fast_uri = __commonJS({
3872
3872
  throw new Error(resolved.error);
3873
3873
  }
3874
3874
  schemelessOptions.skipEscape = true;
3875
- return serialize2(resolved, schemelessOptions);
3875
+ return serialize(resolved, schemelessOptions);
3876
3876
  }
3877
3877
  function resolveComponent(base, relative, options, skipNormalization) {
3878
3878
  const target = {};
3879
3879
  if (!skipNormalization) {
3880
- base = parse3(serialize2(base, options), options);
3881
- relative = parse3(serialize2(relative, options), options);
3880
+ base = parse3(serialize(base, options), options);
3881
+ relative = parse3(serialize(relative, options), options);
3882
3882
  }
3883
3883
  options = options || {};
3884
3884
  if (!options.tolerant && relative.scheme) {
@@ -3932,7 +3932,7 @@ var require_fast_uri = __commonJS({
3932
3932
  const normalizedB = normalizeComparableURI(uriB, options);
3933
3933
  return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA === normalizedB;
3934
3934
  }
3935
- function serialize2(cmpts, opts) {
3935
+ function serialize(cmpts, opts) {
3936
3936
  const component = {
3937
3937
  host: cmpts.host,
3938
3938
  scheme: cmpts.scheme,
@@ -4186,7 +4186,7 @@ var require_fast_uri = __commonJS({
4186
4186
  function normalizeStringWithStatus(uri, opts) {
4187
4187
  const { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = parseWithStatus(uri, opts);
4188
4188
  return {
4189
- normalized: malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? uri : serialize2(parsed, opts),
4189
+ normalized: malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? uri : serialize(parsed, opts),
4190
4190
  malformedAuthorityOrPort,
4191
4191
  malformedPercentEncoding,
4192
4192
  malformedSchemeSpecific,
@@ -4200,7 +4200,7 @@ var require_fast_uri = __commonJS({
4200
4200
  }
4201
4201
  let value;
4202
4202
  try {
4203
- value = typeof uri === "string" ? uri : serialize2(uri, opts);
4203
+ value = typeof uri === "string" ? uri : serialize(uri, opts);
4204
4204
  } catch {
4205
4205
  return void 0;
4206
4206
  }
@@ -4213,7 +4213,7 @@ var require_fast_uri = __commonJS({
4213
4213
  resolve,
4214
4214
  resolveComponent,
4215
4215
  equal,
4216
- serialize: serialize2,
4216
+ serialize,
4217
4217
  parse: parse3
4218
4218
  };
4219
4219
  module.exports = fastUri;
@@ -7199,16 +7199,6 @@ var require_dist = __commonJS({
7199
7199
  }
7200
7200
  });
7201
7201
 
7202
- // src/index.ts
7203
- import { execFile } from "node:child_process";
7204
- import { existsSync as existsSync5, readdirSync } from "node:fs";
7205
- import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
7206
- import { createHash } from "node:crypto";
7207
- import { tmpdir } from "node:os";
7208
- import path5 from "node:path";
7209
- import { fileURLToPath as fileURLToPath3 } from "node:url";
7210
- import { promisify } from "node:util";
7211
-
7212
7202
  // ../../node_modules/zod/v3/external.js
7213
7203
  var external_exports = {};
7214
7204
  __export(external_exports, {
@@ -7700,15 +7690,15 @@ var makeIssue = (params) => {
7700
7690
  message: issueData.message
7701
7691
  };
7702
7692
  }
7703
- let errorMessage = "";
7693
+ let errorMessage2 = "";
7704
7694
  const maps = errorMaps.filter((m) => !!m).slice().reverse();
7705
7695
  for (const map of maps) {
7706
- errorMessage = map(fullIssue, { data, defaultError: errorMessage }).message;
7696
+ errorMessage2 = map(fullIssue, { data, defaultError: errorMessage2 }).message;
7707
7697
  }
7708
7698
  return {
7709
7699
  ...issueData,
7710
7700
  path: fullPath,
7711
- message: errorMessage
7701
+ message: errorMessage2
7712
7702
  };
7713
7703
  };
7714
7704
  var EMPTY_PATH = [];
@@ -17546,19 +17536,19 @@ var getRefs = (options) => {
17546
17536
  };
17547
17537
 
17548
17538
  // ../../node_modules/zod-to-json-schema/dist/esm/errorMessages.js
17549
- function addErrorMessage(res, key, errorMessage, refs) {
17539
+ function addErrorMessage(res, key, errorMessage2, refs) {
17550
17540
  if (!refs?.errorMessages)
17551
17541
  return;
17552
- if (errorMessage) {
17542
+ if (errorMessage2) {
17553
17543
  res.errorMessage = {
17554
17544
  ...res.errorMessage,
17555
- [key]: errorMessage
17545
+ [key]: errorMessage2
17556
17546
  };
17557
17547
  }
17558
17548
  }
17559
- function setResponseValueAndErrors(res, key, value, errorMessage, refs) {
17549
+ function setResponseValueAndErrors(res, key, value, errorMessage2, refs) {
17560
17550
  res[key] = value;
17561
- addErrorMessage(res, key, errorMessage, refs);
17551
+ addErrorMessage(res, key, errorMessage2, refs);
17562
17552
  }
17563
17553
 
17564
17554
  // ../../node_modules/zod-to-json-schema/dist/esm/getRelativePath.js
@@ -18869,8 +18859,8 @@ var Protocol = class {
18869
18859
  if (queuedMessage.type === "response") {
18870
18860
  resolver(message);
18871
18861
  } else {
18872
- const errorMessage = message;
18873
- const error2 = new McpError(errorMessage.error.code, errorMessage.error.message, errorMessage.error.data);
18862
+ const errorMessage2 = message;
18863
+ const error2 = new McpError(errorMessage2.error.code, errorMessage2.error.message, errorMessage2.error.data);
18874
18864
  resolver(error2);
18875
18865
  }
18876
18866
  } else {
@@ -20161,23 +20151,23 @@ var Server = class extends Protocol {
20161
20151
  const wrappedHandler = async (request, extra) => {
20162
20152
  const validatedRequest = safeParse2(CallToolRequestSchema, request);
20163
20153
  if (!validatedRequest.success) {
20164
- const errorMessage = validatedRequest.error instanceof Error ? validatedRequest.error.message : String(validatedRequest.error);
20165
- throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call request: ${errorMessage}`);
20154
+ const errorMessage2 = validatedRequest.error instanceof Error ? validatedRequest.error.message : String(validatedRequest.error);
20155
+ throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call request: ${errorMessage2}`);
20166
20156
  }
20167
20157
  const { params } = validatedRequest.data;
20168
20158
  const result = await Promise.resolve(handler(request, extra));
20169
20159
  if (params.task) {
20170
20160
  const taskValidationResult = safeParse2(CreateTaskResultSchema, result);
20171
20161
  if (!taskValidationResult.success) {
20172
- const errorMessage = taskValidationResult.error instanceof Error ? taskValidationResult.error.message : String(taskValidationResult.error);
20173
- throw new McpError(ErrorCode.InvalidParams, `Invalid task creation result: ${errorMessage}`);
20162
+ const errorMessage2 = taskValidationResult.error instanceof Error ? taskValidationResult.error.message : String(taskValidationResult.error);
20163
+ throw new McpError(ErrorCode.InvalidParams, `Invalid task creation result: ${errorMessage2}`);
20174
20164
  }
20175
20165
  return taskValidationResult.data;
20176
20166
  }
20177
20167
  const validationResult = safeParse2(CallToolResultSchema, result);
20178
20168
  if (!validationResult.success) {
20179
- const errorMessage = validationResult.error instanceof Error ? validationResult.error.message : String(validationResult.error);
20180
- throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call result: ${errorMessage}`);
20169
+ const errorMessage2 = validationResult.error instanceof Error ? validationResult.error.message : String(validationResult.error);
20170
+ throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call result: ${errorMessage2}`);
20181
20171
  }
20182
20172
  return validationResult.data;
20183
20173
  };
@@ -20671,12 +20661,12 @@ var McpServer = class {
20671
20661
  * @param errorMessage - The error message.
20672
20662
  * @returns The tool error result.
20673
20663
  */
20674
- createToolError(errorMessage) {
20664
+ createToolError(errorMessage2) {
20675
20665
  return {
20676
20666
  content: [
20677
20667
  {
20678
20668
  type: "text",
20679
- text: errorMessage
20669
+ text: errorMessage2
20680
20670
  }
20681
20671
  ],
20682
20672
  isError: true
@@ -20694,8 +20684,8 @@ var McpServer = class {
20694
20684
  const parseResult = await safeParseAsync2(schemaToParse, args);
20695
20685
  if (!parseResult.success) {
20696
20686
  const error2 = "error" in parseResult ? parseResult.error : "Unknown error";
20697
- const errorMessage = getParseErrorMessage(error2);
20698
- throw new McpError(ErrorCode.InvalidParams, `Input validation error: Invalid arguments for tool ${toolName}: ${errorMessage}`);
20687
+ const errorMessage2 = getParseErrorMessage(error2);
20688
+ throw new McpError(ErrorCode.InvalidParams, `Input validation error: Invalid arguments for tool ${toolName}: ${errorMessage2}`);
20699
20689
  }
20700
20690
  return parseResult.data;
20701
20691
  }
@@ -20719,8 +20709,8 @@ var McpServer = class {
20719
20709
  const parseResult = await safeParseAsync2(outputObj, result.structuredContent);
20720
20710
  if (!parseResult.success) {
20721
20711
  const error2 = "error" in parseResult ? parseResult.error : "Unknown error";
20722
- const errorMessage = getParseErrorMessage(error2);
20723
- throw new McpError(ErrorCode.InvalidParams, `Output validation error: Invalid structured content for tool ${toolName}: ${errorMessage}`);
20712
+ const errorMessage2 = getParseErrorMessage(error2);
20713
+ throw new McpError(ErrorCode.InvalidParams, `Output validation error: Invalid structured content for tool ${toolName}: ${errorMessage2}`);
20724
20714
  }
20725
20715
  }
20726
20716
  /**
@@ -20932,8 +20922,8 @@ var McpServer = class {
20932
20922
  const parseResult = await safeParseAsync2(argsObj, request.params.arguments);
20933
20923
  if (!parseResult.success) {
20934
20924
  const error2 = "error" in parseResult ? parseResult.error : "Unknown error";
20935
- const errorMessage = getParseErrorMessage(error2);
20936
- throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for prompt ${request.params.name}: ${errorMessage}`);
20925
+ const errorMessage2 = getParseErrorMessage(error2);
20926
+ throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for prompt ${request.params.name}: ${errorMessage2}`);
20937
20927
  }
20938
20928
  const args = parseResult.data;
20939
20929
  const cb = prompt.callback;
@@ -21443,33 +21433,47 @@ var StdioServerTransport = class {
21443
21433
  }
21444
21434
  };
21445
21435
 
21436
+ // src/native.ts
21437
+ import { createRequire } from "node:module";
21438
+ import native from "../native/index.js";
21439
+ var { createScope, getScope, forkScope, deleteScope, warmModelRequest, doctor, machine, health, status, usage, createSession, deleteSession, killSession, listModels, route, runTaskRequest, runTaskBatchRequest, listTaskJobs, getTaskJob, cancelTaskJob, removeTaskJob } = native;
21440
+ var { version: SERVER_VERSION } = createRequire(import.meta.url)("../package.json");
21441
+
21442
+ // src/lifecycle.ts
21443
+ import { spawn as spawn2 } from "node:child_process";
21444
+
21445
+ // src/serve.ts
21446
+ import { spawn } from "node:child_process";
21447
+ import { existsSync as existsSync2 } from "node:fs";
21448
+ import { createRequire as createRequire2 } from "node:module";
21449
+ import path2 from "node:path";
21450
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
21451
+
21446
21452
  // src/config.ts
21447
21453
  import { existsSync } from "node:fs";
21448
21454
  import { realpath } from "node:fs/promises";
21449
21455
  import path from "node:path";
21450
21456
  import { fileURLToPath } from "node:url";
21451
21457
  import { homedir } from "node:os";
21458
+ var PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
21452
21459
  function findRepoRoot() {
21453
- const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
21454
21460
  let dir = path.dirname(fileURLToPath(import.meta.url));
21455
21461
  for (let depth = 0; depth < 10; depth += 1) {
21456
- if (existsSync(path.join(dir, "Cargo.toml")) && path.join(dir, "packages", "mcp") === packageRoot) return dir;
21462
+ if (existsSync(path.join(dir, "Cargo.toml")) && path.join(dir, "packages", "mcp") === PACKAGE_ROOT) return dir;
21457
21463
  const parent = path.dirname(dir);
21458
21464
  if (parent === dir) break;
21459
21465
  dir = parent;
21460
21466
  }
21461
21467
  return void 0;
21462
21468
  }
21463
- var REPO_ROOT = findRepoRoot() ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
21464
- var PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
21469
+ var CHECKOUT_ROOT = findRepoRoot();
21470
+ var REPO_ROOT = CHECKOUT_ROOT ?? PACKAGE_ROOT;
21465
21471
  function adapterPath(file) {
21466
- const bundled = path.join(PACKAGE_ROOT, "adapters", file);
21467
- if (existsSync(bundled)) return bundled;
21468
- return path.join(REPO_ROOT, "benchmark/local/scripts", file);
21472
+ return path.join(PACKAGE_ROOT, "dist", "research", file);
21469
21473
  }
21470
21474
  var RESEARCH_ADAPTERS = {
21471
- bash: adapterPath("bash_agent.py"),
21472
- octocode: adapterPath("octocode_agent.py")
21475
+ bash: adapterPath("bash-agent.js"),
21476
+ octocode: adapterPath("octocode-agent.js")
21473
21477
  };
21474
21478
  var DEFAULT_RESEARCH_ADAPTER = process.env.FREELLAMA_MCP_DEFAULT_ADAPTER === "octocode" ? "octocode" : "bash";
21475
21479
  function envInt(name, fallback) {
@@ -21494,7 +21498,7 @@ function parseAllowedResearchRoots(raw, delimiter = path.delimiter) {
21494
21498
  return raw.split(delimiter).filter(Boolean).map((root) => path.resolve(root));
21495
21499
  }
21496
21500
  var ALLOWED_RESEARCH_ROOTS = parseAllowedResearchRoots(
21497
- process.env.FREELLAMA_MCP_ALLOWED_ROOTS ?? findRepoRoot() ?? ""
21501
+ process.env.FREELLAMA_MCP_ALLOWED_ROOTS ?? CHECKOUT_ROOT ?? ""
21498
21502
  );
21499
21503
  var resolvedRootsPromise = null;
21500
21504
  function allowedResearchRoots() {
@@ -21566,8 +21570,7 @@ var DELEGATE_ENV_ALLOWLIST = /* @__PURE__ */ new Set([
21566
21570
  "REQUESTS_CA_BUNDLE",
21567
21571
  "XDG_CACHE_HOME",
21568
21572
  "XDG_DATA_HOME",
21569
- "npm_config_cache",
21570
- "PYTHONPATH"
21573
+ "npm_config_cache"
21571
21574
  ]);
21572
21575
  function delegateEnvironment(source = process.env) {
21573
21576
  const env = {};
@@ -21577,18 +21580,7 @@ function delegateEnvironment(source = process.env) {
21577
21580
  return env;
21578
21581
  }
21579
21582
 
21580
- // src/native.ts
21581
- import { createRequire } from "node:module";
21582
- import native from "../native/index.js";
21583
- var { createScope, getScope, forkScope, deleteScope, warmModelRequest, doctor, machine, health, status, usage, createSession, deleteSession, killSession, listModels, route, runTask, runTaskRequest, runTaskBatchRequest, listTaskJobs, getTaskJob, cancelTaskJob, removeTaskJob } = native;
21584
- var { version: SERVER_VERSION } = createRequire(import.meta.url)("../package.json");
21585
-
21586
21583
  // src/serve.ts
21587
- import { spawn } from "node:child_process";
21588
- import { existsSync as existsSync2 } from "node:fs";
21589
- import { createRequire as createRequire2 } from "node:module";
21590
- import path2 from "node:path";
21591
- import { fileURLToPath as fileURLToPath2 } from "node:url";
21592
21584
  var HEALTH_PATH = "/_freellama/v1/health";
21593
21585
  var STARTUP_TIMEOUT_MS = 15e3;
21594
21586
  var child = null;
@@ -21637,6 +21629,12 @@ function serveBinaryCandidates() {
21637
21629
  if (process.env.FREELLAMA_SERVE_BINARY) candidates.unshift(process.env.FREELLAMA_SERVE_BINARY);
21638
21630
  return candidates;
21639
21631
  }
21632
+ function startServeInBackground() {
21633
+ ensureServe(void 0).catch((error2) => {
21634
+ process.stderr.write(`[freellama serve] background start failed: ${error2 instanceof Error ? error2.message : String(error2)}
21635
+ `);
21636
+ });
21637
+ }
21640
21638
  function stopAutostartedServe() {
21641
21639
  if (child && child.exitCode === null) child.kill("SIGTERM");
21642
21640
  child = null;
@@ -21684,35 +21682,159 @@ async function ensureServe(endpoint) {
21684
21682
  await starting;
21685
21683
  }
21686
21684
 
21687
- // src/helpers.ts
21688
- import { existsSync as existsSync3, realpathSync, statSync } from "node:fs";
21689
- import path3 from "node:path";
21690
- function extractExistingWorkspacePath(command, workspace) {
21691
- let root;
21692
- try {
21693
- root = realpathSync(workspace);
21694
- } catch {
21695
- return null;
21696
- }
21697
- const tokens = command.match(/(?:[^\s"'\\]+|"(?:\\.|[^"])*"|'[^']*')+/g) ?? [];
21698
- const patternFlags = /* @__PURE__ */ new Set(["-name", "-iname", "-path", "-ipath", "-regex", "-iregex", "-wholename"]);
21699
- for (let index = 0; index < tokens.length; index += 1) {
21700
- const raw = tokens[index] ?? "";
21701
- if (index > 0 && patternFlags.has(tokens[index - 1] ?? "")) continue;
21702
- const token = raw.replace(/^["']|["']$/g, "").replace(/[|;&)]+$/g, "").replace(/:\d+(?::\d+)?$/, "");
21703
- if (!token || token.startsWith("-") || /[*?$`]/.test(token)) continue;
21704
- const candidate = path3.isAbsolute(token) ? token : path3.resolve(root, token);
21705
- if (!existsSync3(candidate)) continue;
21685
+ // src/lifecycle.ts
21686
+ var liveDelegates = /* @__PURE__ */ new Set();
21687
+ var DETACH_DELEGATES = process.platform !== "win32";
21688
+ function killDelegateTree(child2) {
21689
+ if (DETACH_DELEGATES && child2.pid !== void 0) {
21706
21690
  try {
21707
- const resolved = realpathSync(candidate);
21708
- const relative = path3.relative(root, resolved);
21709
- const withinRoot = relative && relative !== ".." && !relative.startsWith(`..${path3.sep}`);
21710
- if (withinRoot && statSync(resolved).isFile()) return relative;
21691
+ process.kill(-child2.pid, "SIGKILL");
21692
+ return;
21711
21693
  } catch {
21712
21694
  }
21713
21695
  }
21714
- return null;
21696
+ try {
21697
+ child2.kill("SIGKILL");
21698
+ } catch {
21699
+ }
21700
+ }
21701
+ function runDelegate(file, args, options) {
21702
+ const child2 = spawn2(file, args, { env: options.env, detached: DETACH_DELEGATES, stdio: ["ignore", "pipe", "pipe"] });
21703
+ const done = new Promise((resolve, reject) => {
21704
+ const out = [];
21705
+ const err = [];
21706
+ let outLen = 0;
21707
+ let errLen = 0;
21708
+ let failure;
21709
+ let timedOut = false;
21710
+ let settled = false;
21711
+ const fail = (error2) => {
21712
+ if (failure === void 0) failure = error2;
21713
+ killDelegateTree(child2);
21714
+ };
21715
+ const timer = setTimeout(() => {
21716
+ timedOut = true;
21717
+ killDelegateTree(child2);
21718
+ }, options.timeoutMs);
21719
+ const onAbort = () => {
21720
+ const abort = new Error("The operation was aborted", { cause: options.signal?.reason });
21721
+ abort.name = "AbortError";
21722
+ abort.code = "ABORT_ERR";
21723
+ fail(abort);
21724
+ };
21725
+ if (options.signal?.aborted) onAbort();
21726
+ else options.signal?.addEventListener("abort", onAbort, { once: true });
21727
+ const collect = (chunks, name) => (chunk) => {
21728
+ const length = name === "stdout" ? outLen += chunk.length : errLen += chunk.length;
21729
+ if (length > options.maxBuffer) {
21730
+ const overflow = new RangeError(`${name} maxBuffer length exceeded`);
21731
+ overflow.code = "ERR_CHILD_PROCESS_STDIO_MAXBUFFER";
21732
+ fail(overflow);
21733
+ return;
21734
+ }
21735
+ chunks.push(chunk);
21736
+ };
21737
+ child2.stdout.on("data", collect(out, "stdout"));
21738
+ child2.stderr.on("data", collect(err, "stderr"));
21739
+ const finish = (code, signal, spawnError) => {
21740
+ if (settled) return;
21741
+ settled = true;
21742
+ clearTimeout(timer);
21743
+ options.signal?.removeEventListener("abort", onAbort);
21744
+ const stdout = Buffer.concat(out).toString("utf8");
21745
+ const stderr = Buffer.concat(err).toString("utf8");
21746
+ if (spawnError ?? failure) {
21747
+ reject(spawnError ?? failure);
21748
+ return;
21749
+ }
21750
+ if (code === 0 && signal === null) {
21751
+ resolve({ stdout, stderr });
21752
+ return;
21753
+ }
21754
+ const error2 = new Error(`Command failed: ${[file, ...args].join(" ")}
21755
+ ${stderr}`);
21756
+ Object.assign(error2, { code, killed: timedOut, signal, stdout, stderr });
21757
+ reject(error2);
21758
+ };
21759
+ child2.once("error", (error2) => finish(null, null, error2));
21760
+ child2.once("close", (code, signal) => finish(code, signal));
21761
+ });
21762
+ return { child: child2, done };
21763
+ }
21764
+ function killLiveDelegates() {
21765
+ for (const child2 of liveDelegates) killDelegateTree(child2);
21766
+ liveDelegates.clear();
21767
+ }
21768
+ function installProcessLifecycle() {
21769
+ for (const stream of [process.stdout, process.stderr]) {
21770
+ stream.on("error", (error2) => {
21771
+ if (error2.code === "EPIPE") process.exit(0);
21772
+ throw error2;
21773
+ });
21774
+ }
21775
+ process.on("exit", () => {
21776
+ killLiveDelegates();
21777
+ stopAutostartedServe();
21778
+ });
21779
+ process.stdin.on("close", () => {
21780
+ killLiveDelegates();
21781
+ stopAutostartedServe();
21782
+ process.exit(0);
21783
+ });
21784
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
21785
+ process.on(signal, () => {
21786
+ killLiveDelegates();
21787
+ process.exit(0);
21788
+ });
21789
+ }
21790
+ }
21791
+
21792
+ // src/schema-dialect.ts
21793
+ function stripSchemaDialect(message) {
21794
+ const tools = message.result?.tools;
21795
+ if (!Array.isArray(tools)) return message;
21796
+ for (const tool of tools) {
21797
+ delete tool.inputSchema?.$schema;
21798
+ delete tool.outputSchema?.$schema;
21799
+ }
21800
+ return message;
21801
+ }
21802
+ function withoutSchemaDialect(transport2) {
21803
+ const send = transport2.send.bind(transport2);
21804
+ transport2.send = (message, options) => send(stripSchemaDialect(message), options);
21805
+ return transport2;
21806
+ }
21807
+
21808
+ // src/docs-resources.ts
21809
+ import { existsSync as existsSync3, readdirSync } from "node:fs";
21810
+ import { readFile } from "node:fs/promises";
21811
+ import path3 from "node:path";
21812
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
21813
+ function registerDocsResources(server2) {
21814
+ const PACKAGED_DOCS_DIR = path3.resolve(path3.dirname(fileURLToPath3(import.meta.url)), "..", "docs");
21815
+ if (!existsSync3(path3.join(PACKAGED_DOCS_DIR, "README.md"))) {
21816
+ throw new Error(`FreeLlama MCP documentation index is missing at ${PACKAGED_DOCS_DIR}/README.md; run the package build.`);
21817
+ }
21818
+ const packagedDocs = readdirSync(PACKAGED_DOCS_DIR).filter((name) => name.endsWith(".md")).sort();
21819
+ for (const name of packagedDocs) {
21820
+ const isIndex = name === "README.md";
21821
+ const slug = isIndex ? "index" : name.replace(/\.md$/, "");
21822
+ const uri = `freellama://docs/${slug}`;
21823
+ server2.registerResource(
21824
+ `freellama-docs-${slug.toLowerCase()}`,
21825
+ uri,
21826
+ {
21827
+ mimeType: "text/markdown",
21828
+ description: isIndex ? "Index of packaged FreeLlama operator and agent documentation. Read this first, then fetch one relevant document." : `Packaged FreeLlama documentation: ${name}.`
21829
+ },
21830
+ async () => ({
21831
+ contents: [{ uri, mimeType: "text/markdown", text: await readFile(path3.join(PACKAGED_DOCS_DIR, name), "utf8") }]
21832
+ })
21833
+ );
21834
+ }
21715
21835
  }
21836
+
21837
+ // src/helpers.ts
21716
21838
  async function ollamaFetch(endpoint, path6, init = {}) {
21717
21839
  const base = (endpoint ?? DEFAULT_OLLAMA_ENDPOINT).replace(/\/$/, "");
21718
21840
  const controller = new AbortController();
@@ -21759,62 +21881,6 @@ async function readNdjson(body, onLine) {
21759
21881
  }
21760
21882
  return text + decoder.decode();
21761
21883
  }
21762
- function summarizeOllamaPullStream(text) {
21763
- const events = [];
21764
- for (const line of text.split(/\r?\n/)) {
21765
- const trimmed = line.trim();
21766
- if (!trimmed) continue;
21767
- try {
21768
- const parsed = JSON.parse(trimmed);
21769
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
21770
- events.push(parsed);
21771
- }
21772
- } catch {
21773
- }
21774
- }
21775
- if (events.length === 0) {
21776
- if (!text.trim()) return { status: "empty" };
21777
- try {
21778
- const parsed = JSON.parse(text);
21779
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
21780
- return parsed;
21781
- }
21782
- } catch {
21783
- return { status: "unparsed", raw: clipText(text, 500) };
21784
- }
21785
- return { status: "unparsed", raw: clipText(text, 500) };
21786
- }
21787
- const last = events[events.length - 1] ?? {};
21788
- const withBytes = [...events].reverse().find((event) => typeof event.total === "number" && event.total > 0);
21789
- const completed = typeof withBytes?.completed === "number" ? withBytes.completed : null;
21790
- const total = typeof withBytes?.total === "number" ? withBytes.total : null;
21791
- const percent = completed != null && total != null && total > 0 ? Math.round(completed / total * 1e3) / 10 : null;
21792
- const status2 = typeof last.error === "string" && last.error ? "error" : last.status;
21793
- return {
21794
- ...last,
21795
- ...status2 === void 0 ? {} : { status: status2 },
21796
- progress: {
21797
- events: events.length,
21798
- completed,
21799
- total,
21800
- percent,
21801
- lastStatus: last.status ?? null
21802
- }
21803
- };
21804
- }
21805
- async function ollamaPull(endpoint, model, timeoutSeconds, options = {}) {
21806
- const raw = await ollamaFetch(endpoint, "/api/pull", {
21807
- method: "POST",
21808
- body: { model, stream: true },
21809
- timeoutMs: (timeoutSeconds ?? DEFAULT_PULL_TIMEOUT_SECONDS) * 1e3,
21810
- parse: "ndjson",
21811
- signal: options.signal,
21812
- onLine: options.onProgress
21813
- });
21814
- if (typeof raw === "string") return summarizeOllamaPullStream(raw);
21815
- if (raw && typeof raw === "object" && !Array.isArray(raw)) return raw;
21816
- return { status: "empty" };
21817
- }
21818
21884
  var endpointParam = external_exports.string().min(1).optional();
21819
21885
  var ollamaEndpointParam = external_exports.string().min(1).optional();
21820
21886
  var TASK_KINDS = [
@@ -21835,19 +21901,7 @@ function canonicalTaskKind(task) {
21835
21901
  var objectiveParam = external_exports.enum(["fastest", "balanced", "quality"]).optional();
21836
21902
  var executionPreferenceParam = external_exports.enum(["auto", "prefer_cpu", "prefer_gpu"]).optional();
21837
21903
  var minPlacementEvidenceParam = external_exports.enum(["configured", "observed"]).optional();
21838
- var CONFIDENCE_RANK = { low: 1, medium: 2 };
21839
21904
  var minConfidenceParam = external_exports.enum(["low", "medium"]).optional();
21840
- function belowConfidence(decision, minConfidence) {
21841
- if (!minConfidence) return null;
21842
- const actual = typeof decision.confidence === "string" ? decision.confidence : "low";
21843
- if ((CONFIDENCE_RANK[actual] ?? 0) >= (CONFIDENCE_RANK[minConfidence] ?? 0)) return null;
21844
- return errorResult(
21845
- new Error(
21846
- `Route refused: confidence is "${actual}" (evidence: ${decision.evidence}), below the requested minimum "${minConfidence}". Selected model would have been "${decision.selected_model}" for reasons [${decision.reasons?.join(", ")}].
21847
- This is a fail-closed refusal, not a failure: the local router cannot justify this pick. Escalate to your own model, pass an explicit \`model\`, or run \`bench-all\` and configure a task policy to raise the evidence level.`
21848
- )
21849
- );
21850
- }
21851
21905
  var REQUIRED_CAPABILITIES = [
21852
21906
  "completion",
21853
21907
  "tools",
@@ -21880,7 +21934,8 @@ function taskMessages({ systemPrompt, messages, prompt, images }) {
21880
21934
  return [{ role: "system", content: systemPrompt }, ...conversation];
21881
21935
  }
21882
21936
  var batchTaskParam = external_exports.object({
21883
- task: taskParam.removeDefault().optional().describe("caller owns prompts and output format."),
21937
+ // Nested `task` shares the top-level description; repeating it re-sends the text every request.
21938
+ task: taskParam.removeDefault().optional(),
21884
21939
  objective: objectiveParam,
21885
21940
  model: external_exports.string().min(1).optional(),
21886
21941
  sessionId: external_exports.string().uuid().optional(),
@@ -21893,6 +21948,7 @@ var batchTaskParam = external_exports.object({
21893
21948
  priority: external_exports.enum(["interactive", "normal", "background"]).optional(),
21894
21949
  maxWaitSeconds: external_exports.number().int().positive().optional(),
21895
21950
  timeoutSeconds: external_exports.number().int().positive().optional(),
21951
+ stopOnRepetition: external_exports.boolean().optional().describe("Stream and stop once output repeats itself; keeps all generated text"),
21896
21952
  prompt: external_exports.string().min(1).optional(),
21897
21953
  systemPrompt: systemPromptParam,
21898
21954
  messages: messagesParam,
@@ -21907,56 +21963,27 @@ var batchTaskParam = external_exports.object({
21907
21963
  topLogprobs: external_exports.number().int().nonnegative().optional(),
21908
21964
  minConfidence: minConfidenceParam
21909
21965
  }).strict();
21966
+ function fresh(schema) {
21967
+ const def = { ...schema._def };
21968
+ for (const [key, value] of Object.entries(def)) {
21969
+ if (value instanceof external_exports.ZodType) def[key] = fresh(value);
21970
+ else if (Array.isArray(value) && value.length > 0 && value.every((v) => v instanceof external_exports.ZodType)) {
21971
+ def[key] = value.map((v) => fresh(v));
21972
+ }
21973
+ }
21974
+ if (schema instanceof external_exports.ZodObject) {
21975
+ const shape = Object.fromEntries(Object.entries(schema.shape).map(([k, v]) => [k, fresh(v)]));
21976
+ def.shape = () => shape;
21977
+ }
21978
+ return new schema.constructor(def);
21979
+ }
21980
+ var BATCH_TASK_FIELDS = new Set(Object.keys(batchTaskParam.shape));
21910
21981
  var batchItemParam = external_exports.object({
21911
21982
  id: external_exports.string().min(1),
21912
21983
  independent: external_exports.literal(true),
21913
- task: batchTaskParam
21984
+ task: fresh(batchTaskParam)
21914
21985
  }).strict();
21915
21986
  var objectResultSchema = external_exports.object({}).passthrough();
21916
- var doctorResultSchema = external_exports.object({
21917
- summary: external_exports.string().optional(),
21918
- endpoint: external_exports.string().optional(),
21919
- scheduler: external_exports.unknown().optional()
21920
- }).passthrough();
21921
- var sessionResultSchema = external_exports.object({
21922
- session_id: external_exports.string().optional(),
21923
- deleted: external_exports.boolean().optional(),
21924
- killed: external_exports.boolean().optional(),
21925
- runner_stop_confirmed: external_exports.boolean().optional()
21926
- }).passthrough();
21927
- var manageResultSchema = external_exports.object({
21928
- status: external_exports.string().optional(),
21929
- progress: external_exports.unknown().optional()
21930
- }).passthrough();
21931
- var deleteResultSchema = external_exports.object({
21932
- deleted: external_exports.string()
21933
- }).passthrough();
21934
- var modelsResultSchema = external_exports.object({
21935
- models: external_exports.array(external_exports.record(external_exports.unknown())).optional(),
21936
- tags: external_exports.array(external_exports.record(external_exports.unknown())).optional(),
21937
- page: external_exports.unknown().optional(),
21938
- knowledge: external_exports.unknown().optional(),
21939
- library: external_exports.unknown().optional()
21940
- }).passthrough();
21941
- var taskResultSchema = external_exports.object({
21942
- selected_model: external_exports.string().optional(),
21943
- context_window_fit: external_exports.string().optional(),
21944
- execution: external_exports.unknown().optional(),
21945
- response: external_exports.unknown().optional(),
21946
- telemetry: external_exports.unknown().optional()
21947
- }).passthrough();
21948
- var batchResultSchema = external_exports.object({
21949
- results: external_exports.unknown().optional(),
21950
- telemetry: external_exports.unknown().optional()
21951
- }).passthrough();
21952
- var researchResultSchema = external_exports.object({
21953
- answer: external_exports.string(),
21954
- summary: external_exports.string(),
21955
- citations: external_exports.unknown().optional(),
21956
- verification: external_exports.unknown(),
21957
- usage: external_exports.unknown().optional(),
21958
- telemetry: external_exports.unknown().optional()
21959
- }).passthrough();
21960
21987
  function configuredExternalCost(env = process.env) {
21961
21988
  const model = env.FREELLAMA_EXTERNAL_COST_MODEL;
21962
21989
  const inputRaw = env.FREELLAMA_EXTERNAL_COST_INPUT_USD_PER_M;
@@ -22013,11 +22040,6 @@ function clipText(text, limit) {
22013
22040
  const head = Math.floor(usable * 2 / 3);
22014
22041
  return text.slice(0, head) + marker + text.slice(-(usable - head));
22015
22042
  }
22016
- var PRETTY_PRINT_MAX_BYTES = 8 * 1024;
22017
- function serialize(value) {
22018
- const compact = JSON.stringify(value);
22019
- return compact.length > PRETTY_PRINT_MAX_BYTES ? compact : JSON.stringify(value, null, 2);
22020
- }
22021
22043
  function resultSummary(value) {
22022
22044
  const explicit = value.summary;
22023
22045
  if (typeof explicit === "string" && explicit.trim()) return clipText(explicit.trim(), 500);
@@ -22036,7 +22058,7 @@ function taskAnswerText(payload) {
22036
22058
  const message = record2(response.message);
22037
22059
  if (message) {
22038
22060
  const parts = [];
22039
- if (typeof message.content === "string" && message.content.trim()) parts.push(message.content.trim());
22061
+ if (typeof message.content === "string" && message.content.trim()) parts.push(answerContent(message.content, payload));
22040
22062
  if (Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {
22041
22063
  parts.push(`tool_calls: ${JSON.stringify(message.tool_calls)}`);
22042
22064
  }
@@ -22052,6 +22074,15 @@ function taskAnswerText(payload) {
22052
22074
  }
22053
22075
  return void 0;
22054
22076
  }
22077
+ function answerContent(content, payload) {
22078
+ const check2 = record2(payload.output_check);
22079
+ const keep = check2?.unique_prefix_chars;
22080
+ if (check2?.repetition_loop !== true || typeof keep !== "number") return content.trim();
22081
+ const characters = Array.from(content);
22082
+ const trimmed = characters.slice(0, keep).join("").trim();
22083
+ return `${trimmed}
22084
+ [freellama: trimmed ${characters.length - keep} repeated characters; the model looped until num_predict \u2014 treat as low confidence]`;
22085
+ }
22055
22086
  function batchAnswerText(payload) {
22056
22087
  if (!Array.isArray(payload.results)) return void 0;
22057
22088
  const perItem = Math.max(512, Math.floor(ANSWER_TEXT_MAX_CHARS / Math.max(payload.results.length, 1)));
@@ -22067,7 +22098,7 @@ ${clipText(answer, perItem)}`;
22067
22098
  return lines.join("\n\n");
22068
22099
  }
22069
22100
  function structuredResult(value, options = {}) {
22070
- const text = options.text ?? (options.legacyJson ? serialize(value) : resultSummary(value));
22101
+ const text = options.text ?? resultSummary(value);
22071
22102
  return {
22072
22103
  content: [{ type: "text", text }],
22073
22104
  structuredContent: value
@@ -22093,6 +22124,19 @@ ${clipText(raw, 2e3)}`
22093
22124
  function isStructuredSuccess(result) {
22094
22125
  return !("isError" in result && result.isError) && "structuredContent" in result && result.structuredContent !== void 0;
22095
22126
  }
22127
+ var escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22128
+ var SERVE_ADDRESS_PATTERN = (() => {
22129
+ let url;
22130
+ try {
22131
+ url = new URL(DEFAULT_SERVE_ENDPOINT);
22132
+ } catch {
22133
+ return /_freellama/i;
22134
+ }
22135
+ const { hostname: hostname2, port } = url;
22136
+ const hosts = ["127.0.0.1", "localhost"].includes(hostname2) ? ["127.0.0.1", "localhost"] : [hostname2];
22137
+ const addresses = hosts.map((host) => escapeRegExp(port ? `${host}:${port}` : host));
22138
+ return new RegExp(`(?:_freellama|${addresses.join("|")})`, "i");
22139
+ })();
22096
22140
  function errorResult(error2) {
22097
22141
  const message = error2 instanceof Error ? error2.message : String(error2);
22098
22142
  let receipt;
@@ -22103,7 +22147,7 @@ function errorResult(error2) {
22103
22147
  }
22104
22148
  } catch {
22105
22149
  }
22106
- const managedServeUnavailable = /(?:connection refused|connect error|failed to connect|error sending request)/i.test(message) && /(?:_freellama|127\.0\.0\.1:11435|localhost:11435)/i.test(message);
22150
+ const managedServeUnavailable = /(?:connection refused|connect error|failed to connect|error sending request)/i.test(message) && SERVE_ADDRESS_PATTERN.test(message);
22107
22151
  const ollamaUnavailable = !managedServeUnavailable && /(?:connection refused|connect error|failed to connect|error sending request)/i.test(message) && /\/api\/(?:tags|ps|chat|generate|embed|show)/.test(message);
22108
22152
  const actionableMessage = managedServeUnavailable ? `${message}
22109
22153
 
@@ -22116,37 +22160,6 @@ Ollama is not running at that address. Ask the user to start Ollama (the app, or
22116
22160
  ...receipt ? { structuredContent: receipt } : {}
22117
22161
  };
22118
22162
  }
22119
- var adapterCallSchema = external_exports.object({
22120
- raw_name: external_exports.string().optional(),
22121
- status: external_exports.string().optional(),
22122
- arguments: external_exports.object({
22123
- tool: external_exports.string().optional(),
22124
- command: external_exports.string().optional(),
22125
- queries: external_exports.object({ path: external_exports.string().optional() }).passthrough().optional()
22126
- }).passthrough().optional()
22127
- }).passthrough();
22128
- var adapterResultSchema = external_exports.object({
22129
- final_answer: external_exports.string(),
22130
- tool_calls: external_exports.array(adapterCallSchema).default([]),
22131
- usage: external_exports.object({
22132
- input_tokens: external_exports.number().nullable().optional(),
22133
- output_tokens: external_exports.number().nullable().optional()
22134
- }).passthrough().default({}),
22135
- model_metadata: external_exports.object({
22136
- context_compactions: external_exports.number().int().nonnegative().optional(),
22137
- runtime_config: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
22138
- context_management: external_exports.object({
22139
- token_counting: external_exports.enum(["configured_estimate", "model_calibrated_estimate"]),
22140
- estimate_scale: external_exports.number().positive(),
22141
- calibration_samples: external_exports.number().int().nonnegative(),
22142
- pinned_overflow: external_exports.enum(["error", "clip"]),
22143
- compactions: external_exports.number().int().nonnegative()
22144
- }).passthrough().optional()
22145
- }).passthrough().optional()
22146
- });
22147
- function parseAdapterResult(raw) {
22148
- return adapterResultSchema.parse(JSON.parse(raw));
22149
- }
22150
22163
  function summarizeEmbeddings(payload) {
22151
22164
  const response = payload.response;
22152
22165
  if (response === null || typeof response !== "object") return null;
@@ -22168,22 +22181,166 @@ function summarizeEmbeddings(payload) {
22168
22181
  };
22169
22182
  }
22170
22183
 
22171
- // src/model-search.ts
22172
- function decodeHtml(text) {
22173
- const entities = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " ", middot: "\xB7", bull: "\u2022" };
22174
- return text.replace(/&(#x[\da-f]+|#\d+|[a-z]+);/gi, (raw, entity) => {
22175
- if (!entity.startsWith("#")) return entities[entity.toLowerCase()] ?? raw;
22176
- const code = entity[1].toLowerCase() === "x" ? Number.parseInt(entity.slice(2), 16) : Number(entity.slice(1));
22177
- return code > 0 && code <= 1114111 && !(code >= 55296 && code <= 57343) ? String.fromCodePoint(code) : raw;
22178
- });
22179
- }
22180
- function attribute(tag, name) {
22181
- return tag.match(new RegExp(`(?:^|\\s)${name}\\s*=\\s*(["'])([\\s\\S]*?)\\1`, "i"))?.[2];
22182
- }
22183
- function htmlText(html) {
22184
- return decodeHtml(html.replace(/<(script|style|svg)\b[^>]*>[\s\S]*?<\/\1>/gi, "").replace(/<\/(?:p|li|div|h[1-6]|pre|tr)>|<br\s*\/?\s*>/gi, "\n").replace(/<[^>]*>/g, " ")).split("\n").map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n");
22185
- }
22186
- function divContent(html, id) {
22184
+ // src/tools/doctor.ts
22185
+ var doctorResultSchema = external_exports.object({
22186
+ summary: external_exports.string().optional(),
22187
+ endpoint: external_exports.string().optional()
22188
+ }).passthrough();
22189
+ function registerDoctorTool(server2) {
22190
+ server2.registerTool(
22191
+ "doctor",
22192
+ {
22193
+ description: "Use when: checking whether Ollama/FreeLlama are healthy, reading live queues/limits (view:status) or token usage (view:usage). Do not use when: choosing a model (models). Returns: a short summary; pick a view for detail.",
22194
+ inputSchema: external_exports.object({
22195
+ ollamaEndpoint: ollamaEndpointParam,
22196
+ serveEndpoint: endpointParam,
22197
+ view: external_exports.enum(["summary", "full", "status", "usage"]).optional(),
22198
+ days: external_exports.number().int().min(1).max(366).optional()
22199
+ }).strict(),
22200
+ outputSchema: doctorResultSchema,
22201
+ annotations: { readOnlyHint: true }
22202
+ },
22203
+ async ({ ollamaEndpoint, serveEndpoint, view, days }) => {
22204
+ try {
22205
+ if (days !== void 0 && view !== "usage") return errorResult(new Error("days is only valid with view: usage."));
22206
+ if (view === "status") return parsedResult(await status(serveEndpoint));
22207
+ if (view === "usage") return parsedResult(await usage(serveEndpoint, days));
22208
+ const report = parsedResult(await doctor(ollamaEndpoint));
22209
+ if (!isStructuredSuccess(report)) return report;
22210
+ try {
22211
+ report.structuredContent.machine = JSON.parse(await machine(serveEndpoint));
22212
+ delete report.structuredContent.machine_unavailable;
22213
+ } catch (error2) {
22214
+ if (report.structuredContent.machine == null) {
22215
+ report.structuredContent.machine_unavailable = `freellama serve unreachable, so no machine profile: ${error2 instanceof Error ? error2.message : String(error2)}`;
22216
+ }
22217
+ }
22218
+ try {
22219
+ report.structuredContent.platform_health = JSON.parse(await health(serveEndpoint));
22220
+ } catch (error2) {
22221
+ report.structuredContent.platform_health_unavailable = `freellama serve unreachable: ${error2 instanceof Error ? error2.message : String(error2)}`;
22222
+ }
22223
+ const full = { ...report.structuredContent };
22224
+ delete full.ollama_env_config;
22225
+ if ((view ?? "summary") === "full") return structuredResult(full);
22226
+ const running = full.running?.models ?? [];
22227
+ const compact = {
22228
+ status: "ok",
22229
+ summary: `Ollama ${String(full.version?.version ?? "unknown")}; ${running.length} resident model(s).`,
22230
+ endpoint: full.endpoint,
22231
+ ollama: { endpoint: full.endpoint, version: full.version, resident_model_count: running.length },
22232
+ machine: full.machine,
22233
+ local_conservative_config_posture: full.local_conservative_config_posture,
22234
+ host_runtime_signals: full.host_runtime_signals,
22235
+ evidence: { runtime: "ollama_api_version_and_ps", configuration: full.ollama_env_config_source },
22236
+ next: 'Use models{view:"installed"} to select a local model; view:"full" has Ollama configuration and admission.'
22237
+ };
22238
+ return structuredResult(compact);
22239
+ } catch (error2) {
22240
+ return errorResult(error2);
22241
+ }
22242
+ }
22243
+ );
22244
+ }
22245
+
22246
+ // src/serve-client.ts
22247
+ var createSession2 = withServe(createSession);
22248
+ var deleteSession2 = withServe(deleteSession);
22249
+ var killSession2 = withServe(killSession);
22250
+ var listModels2 = withServe(listModels);
22251
+ var route2 = withServe(route);
22252
+ var runTaskRequest2 = withServe(runTaskRequest);
22253
+ var runTaskBatchRequest2 = withServe(runTaskBatchRequest);
22254
+ var warmModelRequest2 = withServe(warmModelRequest);
22255
+ var listTaskJobs2 = withServe(listTaskJobs);
22256
+ var getTaskJob2 = withServe(getTaskJob);
22257
+ var cancelTaskJob2 = withServe(cancelTaskJob);
22258
+ var removeTaskJob2 = withServe(removeTaskJob);
22259
+ var createScope2 = withServe(createScope);
22260
+ var getScope2 = withServe(getScope);
22261
+ var forkScope2 = withServe(forkScope);
22262
+ var deleteScope2 = withServe(deleteScope);
22263
+
22264
+ // src/tools/session.ts
22265
+ var sessionResultSchema = external_exports.object({
22266
+ session_id: external_exports.string().optional(),
22267
+ deleted: external_exports.boolean().optional(),
22268
+ killed: external_exports.boolean().optional(),
22269
+ runner_stop_confirmed: external_exports.boolean().optional()
22270
+ }).passthrough();
22271
+ function registerSessionTool(server2) {
22272
+ server2.registerTool(
22273
+ "session",
22274
+ {
22275
+ description: "Use when: several related run_task calls should stay on one model (create, pass sessionId), or to kill a session's in-flight work. Do not use when: cancelling one deferred job (task_jobs), keeping history (scope), or unloading (ollama_manage). Returns: receipt.",
22276
+ inputSchema: external_exports.object({
22277
+ action: external_exports.enum(["create", "delete", "kill"]),
22278
+ sessionId: external_exports.string().uuid().optional(),
22279
+ endpoint: endpointParam
22280
+ }).strict(),
22281
+ outputSchema: sessionResultSchema,
22282
+ annotations: { destructiveHint: true }
22283
+ },
22284
+ async ({ action, sessionId, endpoint }) => {
22285
+ try {
22286
+ if (action === "create") {
22287
+ if (sessionId !== void 0) return errorResult(new Error("sessionId is only valid for action: delete or kill."));
22288
+ return structuredResult(JSON.parse(await createSession2(endpoint)));
22289
+ }
22290
+ if (sessionId === void 0) return errorResult(new Error(`action: ${action} requires sessionId.`));
22291
+ if (action === "kill") return parsedResult(await killSession2(endpoint, sessionId));
22292
+ await deleteSession2(endpoint, sessionId);
22293
+ return structuredResult({ session_id: sessionId, deleted: true });
22294
+ } catch (error2) {
22295
+ return errorResult(error2);
22296
+ }
22297
+ }
22298
+ );
22299
+ }
22300
+
22301
+ // src/paging.ts
22302
+ import { createHash } from "node:crypto";
22303
+ function pageLiveList(items, limit, cursor, identity) {
22304
+ const pageSize = limit ?? 20;
22305
+ const fingerprint = createHash("sha256").update(items.map(identity).join("\n")).digest("base64url").slice(0, 16);
22306
+ let offset = 0;
22307
+ if (cursor) {
22308
+ try {
22309
+ const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
22310
+ if (!Number.isInteger(parsed.offset) || parsed.offset < 0 || parsed.fingerprint !== fingerprint) {
22311
+ throw new Error("invalid or stale cursor");
22312
+ }
22313
+ offset = parsed.offset;
22314
+ } catch {
22315
+ throw new Error("`cursor` is invalid or the model list changed; restart without cursor.");
22316
+ }
22317
+ }
22318
+ const page = items.slice(offset, offset + pageSize);
22319
+ const nextOffset = offset + page.length;
22320
+ return {
22321
+ items: page,
22322
+ returned: page.length,
22323
+ total: items.length,
22324
+ next_cursor: nextOffset < items.length ? Buffer.from(JSON.stringify({ offset: nextOffset, fingerprint }), "utf8").toString("base64url") : null
22325
+ };
22326
+ }
22327
+
22328
+ // src/model-search.ts
22329
+ function decodeHtml(text) {
22330
+ const entities = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " ", middot: "\xB7", bull: "\u2022" };
22331
+ return text.replace(/&(#x[\da-f]+|#\d+|[a-z]+);/gi, (raw, entity) => {
22332
+ if (!entity.startsWith("#")) return entities[entity.toLowerCase()] ?? raw;
22333
+ const code = entity[1].toLowerCase() === "x" ? Number.parseInt(entity.slice(2), 16) : Number(entity.slice(1));
22334
+ return code > 0 && code <= 1114111 && !(code >= 55296 && code <= 57343) ? String.fromCodePoint(code) : raw;
22335
+ });
22336
+ }
22337
+ function attribute(tag, name) {
22338
+ return tag.match(new RegExp(`(?:^|\\s)${name}\\s*=\\s*(["'])([\\s\\S]*?)\\1`, "i"))?.[2];
22339
+ }
22340
+ function htmlText(html) {
22341
+ return decodeHtml(html.replace(/<(script|style|svg)\b[^>]*>[\s\S]*?<\/\1>/gi, "").replace(/<\/(?:p|li|div|h[1-6]|pre|tr)>|<br\s*\/?\s*>/gi, "\n").replace(/<[^>]*>/g, " ")).split("\n").map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n");
22342
+ }
22343
+ function divContent(html, id) {
22187
22344
  const tags = /<\/?div\b[^>]*>/gi;
22188
22345
  let start;
22189
22346
  let depth = 0;
@@ -22227,7 +22384,8 @@ function parseModelSearch(html) {
22227
22384
  const name = link?.[1] ?? link?.[2];
22228
22385
  if (!name) continue;
22229
22386
  if (!/<p\b[^>]*class="max-w-lg/.test(block)) continue;
22230
- const description = block.match(/<p class="max-w-lg[^"]*">([\s\S]*?)<\/p>/)?.[1]?.replace(/<[^>]+>/g, "").replace(/&#39;/g, "'").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/\s+/g, " ").trim() ?? "";
22387
+ const rawDescription = block.match(/<p class="max-w-lg[^"]*">([\s\S]*?)<\/p>/)?.[1];
22388
+ const description = rawDescription === void 0 ? "" : decodeHtml(rawDescription.replace(/<[^>]+>/g, "")).replace(/\s+/g, " ").trim();
22231
22389
  const capabilities = [...block.matchAll(/text-(?:indigo-600|cyan-500)[^>]*>([a-z]+)<\/span>/g)].map(
22232
22390
  (m) => m[1]
22233
22391
  );
@@ -22240,8 +22398,7 @@ function parseModelSearch(html) {
22240
22398
  capabilities,
22241
22399
  pulls: stat("Pulls"),
22242
22400
  tags: stat("Tag"),
22243
- cloudAvailable: capabilities.includes("cloud"),
22244
- cloudOnly: null
22401
+ cloudAvailable: capabilities.includes("cloud")
22245
22402
  });
22246
22403
  }
22247
22404
  return results;
@@ -22284,6 +22441,10 @@ var CACHE_TTL_MS = 60 * 60 * 1e3;
22284
22441
  var FAILURE_TTL_MS = 60 * 1e3;
22285
22442
  var MAX_PAGE_BYTES = 2 * 1024 * 1024;
22286
22443
  var MAX_README_CHARS = 64 * 1024;
22444
+ var errorMessage = (error2) => error2 instanceof Error ? error2.message : String(error2);
22445
+ function isRemoteModel(entry, name) {
22446
+ return Boolean(entry.remote_host || entry.remote_model) || /-cloud$/i.test(name);
22447
+ }
22287
22448
  function libraryReference(model) {
22288
22449
  const match = model.match(/^(?:library\/)?([a-zA-Z0-9][\w.-]*(?:\/[a-zA-Z0-9][\w.-]*)?)(?::([\w.-]+))?$/);
22289
22450
  if (!match || match[1].split("/").some((part) => part === "." || part === "..")) return null;
@@ -22402,7 +22563,7 @@ var OllamaLibraryClient = class {
22402
22563
  } catch (error2) {
22403
22564
  result = {
22404
22565
  status: error2 instanceof LibraryFetchError ? error2.status : "unavailable",
22405
- message: error2 instanceof LibraryFetchError ? error2.message : "Ollama library lookup failed or timed out. Local metadata remains available."
22566
+ message: error2 instanceof LibraryFetchError ? error2.message : `Ollama library lookup failed or timed out (${errorMessage(error2)}). Local metadata remains available.`
22406
22567
  };
22407
22568
  }
22408
22569
  const now = (this.options.now ?? Date.now)();
@@ -22481,7 +22642,7 @@ function modelKnowledge(model, data, record3, includeReadme = false, supportedFe
22481
22642
  quality: "metadata_only",
22482
22643
  note: "Supported features and advertised uses do not establish task quality. Keep exact-model benchmark and policy evidence separate."
22483
22644
  };
22484
- if (data.remote_host || data.remote_model || /-cloud$/i.test(model)) {
22645
+ if (isRemoteModel(data, model)) {
22485
22646
  return { ...local, localModel: false, library: { status: "remote_model", message: "This Ollama entry points to a remote model." } };
22486
22647
  }
22487
22648
  if (!record3) return { ...local, localModel: true, library: { status: "not_requested" } };
@@ -22531,11 +22692,11 @@ async function enrichInstalledModels(models, client, includeLibrary, loadTags) {
22531
22692
  local = { ...model, ...entry };
22532
22693
  if (Array.isArray(entry.capabilities)) featureSource = "ollama_api";
22533
22694
  }
22534
- } catch {
22535
- localError = "The selected Ollama backend's inventory could not be verified; public guidance was withheld.";
22695
+ } catch (error2) {
22696
+ localError = `The selected Ollama backend's inventory could not be verified (${errorMessage(error2)}); public guidance was withheld.`;
22536
22697
  }
22537
22698
  }
22538
- const record3 = includeLibrary && !localError && !local.remote_host && !local.remote_model && !/-cloud$/i.test(name) ? await client.lookup(name) : void 0;
22699
+ const record3 = includeLibrary && !localError && !isRemoteModel(local, name) ? await client.lookup(name) : void 0;
22539
22700
  const knowledge = modelKnowledge(name, local, record3, false, featureSource);
22540
22701
  results[index] = { ...model, knowledge: localError ? { ...knowledge, library: { status: "unverified", message: localError } } : knowledge };
22541
22702
  }
@@ -22543,229 +22704,277 @@ async function enrichInstalledModels(models, client, includeLibrary, loadTags) {
22543
22704
  return results;
22544
22705
  }
22545
22706
 
22546
- // src/delegate.ts
22547
- import { existsSync as existsSync4, readFileSync } from "node:fs";
22548
- import path4 from "node:path";
22549
- function researchErrorResult(error2, diagnostic) {
22550
- const result = errorResult(error2);
22551
- const summary = diagnostic ?? result.content[0].text;
22552
- return {
22553
- ...result,
22554
- content: [{ type: "text", text: summary }],
22555
- structuredContent: {
22556
- ...result.structuredContent ?? { error: summary },
22557
- answer: "",
22558
- summary,
22559
- verification: {
22560
- recommendation: "escalate",
22561
- grounded: false,
22562
- why: "The research request failed; no usable answer was produced."
22563
- }
22707
+ // src/tools/models.ts
22708
+ var modelsResultSchema = external_exports.object({
22709
+ models: external_exports.array(external_exports.record(external_exports.unknown())).optional(),
22710
+ tags: external_exports.array(external_exports.record(external_exports.unknown())).optional(),
22711
+ page: external_exports.unknown().optional(),
22712
+ knowledge: external_exports.unknown().optional(),
22713
+ library: external_exports.unknown().optional()
22714
+ }).passthrough();
22715
+ var ollamaLibrary = new OllamaLibraryClient();
22716
+ var HOST_MEMORY_FIT_FRACTION = 0.6;
22717
+ async function libraryLookup({
22718
+ model,
22719
+ query,
22720
+ capabilities,
22721
+ order,
22722
+ limit,
22723
+ cursor,
22724
+ includeReadme,
22725
+ endpoint,
22726
+ ollamaEndpoint
22727
+ }) {
22728
+ const localState = async () => {
22729
+ let installed = null;
22730
+ let installedUnavailable;
22731
+ let memoryBytes = null;
22732
+ try {
22733
+ const tags = await ollamaFetch(ollamaEndpoint, "/api/tags");
22734
+ installed = new Set((tags.models ?? []).map((m) => m.name ?? ""));
22735
+ } catch (error2) {
22736
+ installedUnavailable = `Local inventory could not be read (${error2 instanceof Error ? error2.message : String(error2)}); installed is null.`;
22564
22737
  }
22565
- };
22566
- }
22567
- var MODEL_EVIDENCE = (() => {
22568
- const configured = process.env.FREELLAMA_MCP_MODEL_EVIDENCE;
22569
- const candidates = [
22570
- configured,
22571
- path4.join(REPO_ROOT, "benchmark/evidence/model-evidence.json")
22572
- ].filter((x) => Boolean(x));
22573
- for (const file of candidates) {
22574
22738
  try {
22575
- if (!existsSync4(file)) continue;
22576
- const parsed = JSON.parse(readFileSync(file, "utf8"));
22577
- return parsed.models ?? {};
22739
+ const profile = JSON.parse(await machine(endpoint));
22740
+ memoryBytes = profile.memory_bytes ?? profile.unified_memory_bytes ?? null;
22578
22741
  } catch {
22579
22742
  }
22743
+ return { installed, installedUnavailable, memoryBytes };
22744
+ };
22745
+ if (!model && cursor !== void 0) {
22746
+ return errorResult(new Error("Library step 1 has no `cursor`; it pages step-2 tags. Use `limit` for search, or pass `model`."));
22580
22747
  }
22581
- return {};
22582
- })();
22583
- function assessDelegatedAnswer(question, evidenceCount, model) {
22584
- const grounded = evidenceCount > 0;
22585
- const evidence = MODEL_EVIDENCE[model];
22586
- if (evidence?.grade === "unusable") {
22587
- return {
22588
- recommendation: "escalate",
22589
- grounded,
22590
- why: `${model} is not viable for research on this machine (${evidence.note}). It answers fast and confidently while being wrong \u2014 a fast wrong answer is not a speed win. Re-run with a ~27B model (see README), or answer it yourself.`,
22591
- measuredBaseRate: `${model}: ${evidence.note}`
22592
- };
22593
- }
22594
- if (!evidence) {
22595
- return {
22596
- recommendation: "verify",
22597
- grounded,
22598
- why: `${model} has no measured accuracy in this repo's benchmarks, so no base rate applies to this answer. Treat it as unverified until it has been evaluated \u2014 accuracy fell off a cliff below ~12B in the models that were measured.`,
22599
- measuredBaseRate: "no measured base rate for this model"
22600
- };
22601
- }
22602
- if (evidence.grade === "weak" && grounded) {
22603
- return {
22604
- recommendation: "verify",
22605
- grounded,
22606
- why: `${model} holds up on simple single-file lookups but not beyond (${evidence.note}). Check the evidence trail, or re-run on a ~27B model (see README) if the answer matters.`,
22607
- measuredBaseRate: `${model}: ${evidence.note}`
22608
- };
22609
- }
22610
- if (!grounded) {
22611
- return {
22612
- recommendation: "escalate",
22613
- grounded: false,
22614
- why: "The model answered without reading a single file, so this is parametric recall, not research \u2014 the failure mode where `run_task` was verified inventing wrong facts about this project's own architecture. Re-ask with a narrower question, or answer it yourself.",
22615
- measuredBaseRate: "ungrounded answers have no measured accuracy \u2014 they were never the tested path"
22616
- };
22617
- }
22618
- const judgmentSignals = /\b(should (we|i|you)|better|best|worth|review|assess|evaluate|improve|opinion|recommend|why is|is it (good|safe|correct)|design|refactor)\b/i;
22619
- if (judgmentSignals.test(question)) {
22620
- return {
22621
- recommendation: "verify",
22622
- grounded: true,
22623
- why: "The question reads as a judgment call (keyword heuristic), which is the ~67%-accurate bucket rather than the 98.9% one \u2014 and the tone is identical either way. Check the evidence trail against the claim before acting on it.",
22624
- measuredBaseRate: `${model}: ~67% on judgment calls vs 98.9% on grounded lookups`
22625
- };
22748
+ try {
22749
+ if (model) {
22750
+ const ref = libraryReference(model);
22751
+ if (!ref) throw new Error("Use an exact Ollama family or namespace/model name, not a URL or path.");
22752
+ const { family } = ref;
22753
+ const library = await ollamaLibrary.lookup(model);
22754
+ if (library.status !== "available") return structuredResult({
22755
+ family,
22756
+ sourcePage: ref.url,
22757
+ tags: [],
22758
+ tagsUnavailable: library.message,
22759
+ library,
22760
+ recommendation: null
22761
+ });
22762
+ const tags = library.tags;
22763
+ const { installed: installed2, installedUnavailable: installedUnavailable2, memoryBytes } = await localState();
22764
+ const budget = memoryBytes ? memoryBytes * HOST_MEMORY_FIT_FRACTION : null;
22765
+ const annotated = tags.map((t) => ({
22766
+ ...t,
22767
+ installed: installed2 ? installed2.has(t.tag) : null,
22768
+ fitsInMemory: budget && t.sizeBytes ? t.sizeBytes <= budget : null,
22769
+ fitScope: "host_memory_budget_only"
22770
+ }));
22771
+ const runnable = annotated.filter((t) => t.fitsInMemory === true && !t.cloud);
22772
+ const isEmbeddingFamily = library.card.capabilities.includes("embedding");
22773
+ const sized = runnable.filter((t) => t.sizeBytes);
22774
+ const best = budget ? isEmbeddingFamily ? sized.sort((a, b) => (a.sizeBytes ?? 0) - (b.sizeBytes ?? 0))[0] : sized.sort((a, b) => (b.sizeBytes ?? 0) - (a.sizeBytes ?? 0))[0] : void 0;
22775
+ const tagPage = pageLiveList(annotated, limit, cursor, (tag) => tag.tag);
22776
+ return structuredResult({
22777
+ family,
22778
+ sourcePage: ref.url,
22779
+ library: {
22780
+ status: library.status,
22781
+ sourceUrl: library.sourceUrl,
22782
+ tagsSourceUrl: library.tagsSourceUrl,
22783
+ fetchedAt: library.fetchedAt,
22784
+ expiresAt: library.expiresAt,
22785
+ cached: library.cached,
22786
+ ...publicModelGuidance(library, void 0, includeReadme)
22787
+ },
22788
+ ...installedUnavailable2 ? { installedUnavailable: installedUnavailable2 } : {},
22789
+ machineMemoryBytes: memoryBytes,
22790
+ fitBudgetBytes: budget,
22791
+ tags: tagPage.items,
22792
+ page: { returned: tagPage.returned, total: tagPage.total, next_cursor: tagPage.next_cursor },
22793
+ recommendationUnavailable: budget ? void 0 : "No machine profile (freellama serve unreachable), so memory fit could not be checked and no tag is recommended. Start serve, or read the sizes yourself.",
22794
+ recommendation: best ? libraryTrialCandidate(best.tag, isEmbeddingFamily) : null
22795
+ });
22796
+ }
22797
+ const params = new URLSearchParams();
22798
+ for (const c of capabilities ?? []) params.append("c", c);
22799
+ if (query) params.set("q", query);
22800
+ if (order === "newest") params.set("o", "newest");
22801
+ const search = await ollamaLibrary.search(params);
22802
+ const url = search.url;
22803
+ const parsed = search.models.slice(0, limit ?? 10);
22804
+ const { installed, installedUnavailable } = await localState();
22805
+ const installedFamilies = installed ? new Set([...installed].map((n) => n.split(":")[0])) : null;
22806
+ return structuredResult({
22807
+ query: url,
22808
+ fetchedAt: search.fetchedAt,
22809
+ ...parsed.length === 0 ? { searchUnavailable: "No result cards were recognized. The query may have no matches or Ollama's markup may have changed; inspect the source URL." } : {},
22810
+ order: order ?? "popular",
22811
+ count: parsed.length,
22812
+ ...installedUnavailable ? { installedUnavailable } : {},
22813
+ nextStep: 'Not pullable yet. Call again with view:"library", model:"<name>" to get tags, sizes and memory fit.',
22814
+ models: parsed.map((m) => ({ ...m, installed: installedFamilies ? installedFamilies.has(m.name) : null }))
22815
+ });
22816
+ } catch (error2) {
22817
+ return errorResult(error2);
22626
22818
  }
22627
- if (evidenceCount > 5) {
22628
- return {
22629
- recommendation: "verify",
22630
- grounded: true,
22631
- why: `${evidenceCount} tool calls is outside the 1-5 file envelope this tool was measured on. Wide searches are where it drifts; spot-check the evidence trail.`,
22632
- measuredBaseRate: `${model}: 98.9% on grounded lookups, measured within a 1-5 file scope`
22633
- };
22634
- }
22635
- return {
22636
- recommendation: "accept",
22637
- grounded: true,
22638
- why: `Grounded in ${evidenceCount} tool call(s) within the measured 1-5 file envelope, the question is lookup-shaped, and ${model} is measured strong for this (${evidence.note}).`,
22639
- measuredBaseRate: `${model}: 98.9% on grounded lookups (100+ questions)`
22640
- };
22641
22819
  }
22642
-
22643
- // src/context-tools.ts
22644
- var routeDefaults = external_exports.object({
22645
- task: taskParam.removeDefault().optional(),
22646
- objective: objectiveParam,
22647
- model: external_exports.string().min(1).optional(),
22648
- contextTokens: external_exports.number().int().positive().optional(),
22649
- executionPreference: executionPreferenceParam,
22650
- minPlacementEvidence: minPlacementEvidenceParam,
22651
- minConfidence: minConfidenceParam,
22652
- requiredCapabilities: requiredCapabilitiesParam
22653
- }).strict();
22654
- var limits = external_exports.object({
22655
- maxMessages: external_exports.number().int().positive().optional(),
22656
- maxBytes: external_exports.number().int().positive().optional(),
22657
- maxEstimatedTokens: external_exports.number().int().positive().optional(),
22658
- ttlSeconds: external_exports.number().int().positive().optional()
22659
- }).strict();
22660
- function scopeBody(messages, defaults, bounds) {
22661
- return {
22662
- messages,
22663
- route_defaults: defaults === void 0 ? void 0 : {
22664
- task: defaults.task === void 0 ? void 0 : canonicalTaskKind(defaults.task),
22665
- objective: defaults.objective,
22666
- model: defaults.model,
22667
- context_tokens: defaults.contextTokens,
22668
- execution_preference: defaults.executionPreference,
22669
- min_placement_evidence: defaults.minPlacementEvidence,
22670
- min_confidence: defaults.minConfidence,
22671
- required_capabilities: defaults.requiredCapabilities
22820
+ function registerModelsTool(server2) {
22821
+ server2.registerTool(
22822
+ "models",
22823
+ {
22824
+ description: "Use when: listing installed or loaded models with capabilities, policy rank and memory fit, inspecting one model, or searching ollama.com (view:library). Do not use when: running work (run_task) or pulling (ollama_manage). Returns: models; library lists families first, then one family's tags.",
22825
+ inputSchema: external_exports.object({
22826
+ view: external_exports.enum(["installed", "resident", "detail", "raw", "library"]).optional(),
22827
+ model: external_exports.string().min(1).optional(),
22828
+ includeVerbose: external_exports.boolean().optional(),
22829
+ includeLibrary: external_exports.boolean().optional(),
22830
+ includeReadme: external_exports.boolean().optional(),
22831
+ query: external_exports.string().min(1).optional(),
22832
+ capabilities: external_exports.array(external_exports.enum(["vision", "tools", "thinking", "embedding", "cloud"])).min(1).max(5).optional(),
22833
+ order: external_exports.enum(["popular", "newest"]).optional(),
22834
+ limit: external_exports.number().int().positive().max(50).optional(),
22835
+ cursor: external_exports.string().min(1).optional(),
22836
+ endpoint: endpointParam,
22837
+ ollamaEndpoint: ollamaEndpointParam
22838
+ }).strict(),
22839
+ outputSchema: modelsResultSchema,
22840
+ annotations: { readOnlyHint: true }
22672
22841
  },
22673
- limits: bounds === void 0 ? void 0 : {
22674
- max_messages: bounds.maxMessages,
22675
- max_bytes: bounds.maxBytes,
22676
- max_estimated_tokens: bounds.maxEstimatedTokens,
22677
- ttl_seconds: bounds.ttlSeconds
22842
+ async ({ view, model, includeVerbose, includeLibrary, includeReadme, query, capabilities, order, limit, cursor, endpoint, ollamaEndpoint }) => {
22843
+ try {
22844
+ const selectedView = view ?? "installed";
22845
+ if (includeLibrary !== void 0 && selectedView !== "installed" && selectedView !== "detail") {
22846
+ return errorResult(new Error('`includeLibrary` is valid only for views "installed" and "detail".'));
22847
+ }
22848
+ if (includeReadme !== void 0 && !(selectedView === "library" && model || selectedView === "detail" && includeLibrary === true)) {
22849
+ return errorResult(new Error('`includeReadme` needs library step 2, or view "detail" with `includeLibrary:true`.'));
22850
+ }
22851
+ if (selectedView !== "library" && [query, capabilities, order].some((value) => value !== void 0)) {
22852
+ return errorResult(new Error(`view "${selectedView}" does not accept library search fields.`));
22853
+ }
22854
+ if (selectedView !== "library" && selectedView !== "raw" && [limit, cursor].some((value) => value !== void 0)) {
22855
+ return errorResult(new Error(`view "${selectedView}" accepts neither pagination nor library search fields.`));
22856
+ }
22857
+ if (selectedView !== "detail" && includeVerbose !== void 0) {
22858
+ return errorResult(new Error('`includeVerbose` is valid only for view "detail".'));
22859
+ }
22860
+ if (selectedView !== "detail" && selectedView !== "library" && model !== void 0) {
22861
+ return errorResult(new Error('`model` is valid only for views "detail" and "library".'));
22862
+ }
22863
+ if (selectedView === "library" && model && [query, capabilities, order].some((value) => value !== void 0)) {
22864
+ return errorResult(
22865
+ new Error("Library step 2 accepts only `model` plus endpoint overrides; omit step-1 search fields.")
22866
+ );
22867
+ }
22868
+ switch (selectedView) {
22869
+ case "raw": {
22870
+ const raw = await ollamaFetch(ollamaEndpoint, "/api/tags");
22871
+ const models = Array.isArray(raw.models) ? raw.models : [];
22872
+ const page = pageLiveList(models, limit, cursor, (model2) => String(model2.name ?? JSON.stringify(model2)));
22873
+ return structuredResult({ ...raw, models: page.items, page: { returned: page.returned, total: page.total, next_cursor: page.next_cursor } });
22874
+ }
22875
+ case "resident": {
22876
+ const managed = parsedResult(await listModels2(endpoint));
22877
+ if (!isStructuredSuccess(managed)) return managed;
22878
+ const data = managed.structuredContent;
22879
+ const models = (data.models ?? []).filter((entry) => entry.resident === true).map((entry) => {
22880
+ const { resident_view: view2, ...rest } = entry;
22881
+ if (!view2) return rest;
22882
+ return {
22883
+ ...rest,
22884
+ placement: {
22885
+ gpu_percent: view2.gpu_percent,
22886
+ assigned: view2.assigned,
22887
+ processor: view2.processor_label,
22888
+ ...view2.warning !== void 0 ? { warning: view2.warning } : {}
22889
+ }
22890
+ };
22891
+ });
22892
+ return structuredResult({ ...data, models });
22893
+ }
22894
+ case "detail": {
22895
+ if (!model) {
22896
+ return errorResult(new Error('view "detail" needs `model` set to an installed tag.'));
22897
+ }
22898
+ const data = await ollamaFetch(ollamaEndpoint, "/api/show", {
22899
+ method: "POST",
22900
+ body: { model }
22901
+ });
22902
+ let local = data;
22903
+ if (includeLibrary && !isRemoteModel(data, model)) {
22904
+ try {
22905
+ const inventory = await ollamaFetch(ollamaEndpoint, "/api/tags");
22906
+ const canonical = libraryReference(model)?.tag;
22907
+ const entry = canonical && inventory.models?.find((entry2) => libraryReference(String(entry2.name ?? entry2.model))?.tag === canonical);
22908
+ if (entry) local = { ...entry, ...data, digest: entry.digest };
22909
+ } catch {
22910
+ }
22911
+ }
22912
+ const library = includeLibrary && !isRemoteModel(local, model) ? await ollamaLibrary.lookup(model) : void 0;
22913
+ const { license, modelfile, ...rest } = data;
22914
+ const modelInfo = rest.model_info ?? {};
22915
+ const contextEntry = Object.entries(modelInfo).find(
22916
+ ([key, value]) => key.endsWith(".context_length") && typeof value === "number"
22917
+ );
22918
+ return structuredResult({
22919
+ ...rest,
22920
+ max_context_length: contextEntry?.[1] ?? null,
22921
+ knowledge: modelKnowledge(model, local, library, includeReadme),
22922
+ ...includeVerbose ? { license, modelfile } : {}
22923
+ });
22924
+ }
22925
+ case "library":
22926
+ return await libraryLookup({ model, query, capabilities, order, limit, cursor, includeReadme, endpoint, ollamaEndpoint });
22927
+ default: {
22928
+ const result = parsedResult(await listModels2(endpoint));
22929
+ if (!isStructuredSuccess(result)) return result;
22930
+ const data = result.structuredContent;
22931
+ return structuredResult({ ...data, models: await enrichInstalledModels(
22932
+ data.models ?? [],
22933
+ ollamaLibrary,
22934
+ includeLibrary ?? false,
22935
+ async (upstream) => (await ollamaFetch(upstream ?? ollamaEndpoint, "/api/tags")).models ?? []
22936
+ ) });
22937
+ }
22938
+ }
22939
+ } catch (error2) {
22940
+ return errorResult(error2);
22941
+ }
22678
22942
  }
22679
- };
22943
+ );
22680
22944
  }
22681
- function registerContextTools(server2) {
22682
- const createScope2 = withServe(createScope);
22683
- const getScope2 = withServe(getScope);
22684
- const forkScope2 = withServe(forkScope);
22685
- const deleteScope2 = withServe(deleteScope);
22686
- const warmModel = withServe(warmModelRequest);
22945
+
22946
+ // src/tools/scope.ts
22947
+ function registerScopeTool(server2) {
22687
22948
  server2.registerTool("scope", {
22688
- description: "Use when: saved history. Do not use when: KV control. Returns: process-local scope metadata; history is opt-in.",
22949
+ description: "Use when: a multi-turn local conversation should keep its message history server-side (create/get/fork/delete), then continue with run_task {scopeId, scopeRevision}. Do not use when: you want model affinity (session) or runner KV control. Returns: process-local scope metadata; message history only with includeMessages:true.",
22689
22950
  inputSchema: external_exports.object({
22690
22951
  action: external_exports.enum(["create", "get", "fork", "delete"]),
22691
22952
  endpoint: endpointParam,
22692
22953
  scopeId: external_exports.string().uuid().optional(),
22693
22954
  revision: external_exports.number().int().nonnegative().optional(),
22694
22955
  messages: messagesParam.unwrap().min(0).optional(),
22695
- routeDefaults: routeDefaults.optional(),
22696
- limits: limits.optional(),
22697
22956
  includeMessages: external_exports.boolean().optional()
22698
22957
  }).strict(),
22699
22958
  outputSchema: objectResultSchema,
22700
22959
  annotations: { destructiveHint: true }
22701
- }, async ({ action, endpoint, scopeId, revision, messages, routeDefaults: defaults, limits: bounds, includeMessages }) => {
22960
+ }, async ({ action, endpoint, scopeId, revision, messages, includeMessages }) => {
22702
22961
  try {
22703
22962
  if (action === "create" !== (scopeId === void 0)) throw new Error("scopeId is required for get/fork/delete, forbidden for create.");
22704
22963
  if (action === "fork" !== (revision !== void 0)) throw new Error("revision is required only for fork.");
22705
22964
  if (includeMessages !== void 0 && action !== "get") throw new Error("includeMessages is valid only for get.");
22706
22965
  if (messages !== void 0 && action !== "create") throw new Error("messages is valid only for create.");
22707
- if ((defaults !== void 0 || bounds !== void 0) && action !== "create" && action !== "fork") throw new Error("routeDefaults and limits are valid only for create/fork.");
22708
22966
  if (action === "delete") {
22709
22967
  await deleteScope2(endpoint, scopeId);
22710
22968
  return structuredResult({ deleted: true, scope_id: scopeId });
22711
22969
  }
22712
- return parsedResult(action === "create" ? await createScope2(endpoint, scopeBody(messages, defaults, bounds)) : action === "fork" ? await forkScope2(endpoint, scopeId, { ...scopeBody(void 0, defaults, bounds), revision }) : await getScope2(endpoint, scopeId, includeMessages));
22713
- } catch (error2) {
22714
- return errorResult(error2);
22715
- }
22716
- });
22717
- server2.registerTool("warm_model", {
22718
- description: "Use when: preload an installed model. Do not use when: generation or pull. Returns: managed residency receipt or job ID.",
22719
- inputSchema: external_exports.object({
22720
- endpoint: endpointParam,
22721
- model: external_exports.string().min(1),
22722
- task: taskParam.removeDefault().optional(),
22723
- contextTokens: external_exports.number().int().positive().optional(),
22724
- executionPreference: executionPreferenceParam,
22725
- minPlacementEvidence: minPlacementEvidenceParam,
22726
- priority: external_exports.enum(["interactive", "normal", "background"]).optional(),
22727
- keepAlive: external_exports.string().min(1).optional().describe("Omitted = adaptive finite TTL."),
22728
- maxWaitSeconds: external_exports.number().int().positive().optional(),
22729
- timeoutSeconds: external_exports.number().int().positive().optional(),
22730
- defer: external_exports.boolean().optional()
22731
- }).strict(),
22732
- outputSchema: objectResultSchema,
22733
- annotations: { destructiveHint: false }
22734
- }, async ({ endpoint, model, task, contextTokens, executionPreference, minPlacementEvidence, priority, keepAlive, maxWaitSeconds, timeoutSeconds, defer }) => {
22735
- try {
22736
- if (task === "embedding") throw new Error("warm_model supports chat models; embedding warming is unsupported.");
22737
- return parsedResult(await warmModel(endpoint, {
22738
- model,
22739
- task: task === void 0 ? void 0 : canonicalTaskKind(task),
22740
- context_tokens: contextTokens,
22741
- execution_preference: executionPreference,
22742
- min_placement_evidence: minPlacementEvidence,
22743
- priority,
22744
- keep_alive: keepAlive,
22745
- max_wait_seconds: maxWaitSeconds,
22746
- timeout_seconds: timeoutSeconds,
22747
- defer
22748
- }));
22970
+ return parsedResult(action === "create" ? await createScope2(endpoint, { messages }) : action === "fork" ? await forkScope2(endpoint, scopeId, { revision }) : await getScope2(endpoint, scopeId, includeMessages));
22749
22971
  } catch (error2) {
22750
22972
  return errorResult(error2);
22751
22973
  }
22752
22974
  });
22753
22975
  }
22754
22976
 
22755
- // src/index.ts
22756
- var execFileAsync = promisify(execFile);
22757
- var createSession2 = withServe(createSession);
22758
- var deleteSession2 = withServe(deleteSession);
22759
- var killSession2 = withServe(killSession);
22760
- var listModels2 = withServe(listModels);
22761
- var ollamaLibrary = new OllamaLibraryClient();
22762
- var route2 = withServe(route);
22763
- var runTaskRequest2 = withServe(runTaskRequest);
22764
- var runTaskBatchRequest2 = withServe(runTaskBatchRequest);
22765
- var listTaskJobs2 = withServe(listTaskJobs);
22766
- var getTaskJob2 = withServe(getTaskJob);
22767
- var cancelTaskJob2 = withServe(cancelTaskJob);
22768
- var removeTaskJob2 = withServe(removeTaskJob);
22977
+ // src/telemetry.ts
22769
22978
  var EXTERNAL_COST = configuredExternalCost();
22770
22979
  function withTaskTelemetry(result) {
22771
22980
  if (!isStructuredSuccess(result)) return result;
@@ -22808,55 +23017,469 @@ function withBatchTelemetry(result) {
22808
23017
  telemetry: complete === rows.filter((row) => row?.ok === true).length ? costTelemetry({ inputTokens, outputTokens }, EXTERNAL_COST) : { local: null, externalEquivalent: null, note: "Batch aggregate unavailable because one or more successful items omitted token counts." }
22809
23018
  }, { text: batchAnswerText(payload) });
22810
23019
  }
22811
- function pageLiveList(items, limit, cursor, identity) {
22812
- const pageSize = limit ?? 20;
22813
- const fingerprint = createHash("sha256").update(items.map(identity).join("\n")).digest("base64url").slice(0, 16);
22814
- let offset = 0;
22815
- if (cursor) {
23020
+
23021
+ // src/tools/run-task.ts
23022
+ var taskResultSchema = external_exports.object({
23023
+ selected_model: external_exports.string().optional(),
23024
+ context_window_fit: external_exports.string().optional(),
23025
+ execution: external_exports.unknown().optional(),
23026
+ response: external_exports.unknown().optional(),
23027
+ telemetry: external_exports.unknown().optional()
23028
+ }).passthrough();
23029
+ var batchResultSchema = external_exports.object({
23030
+ results: external_exports.unknown().optional(),
23031
+ telemetry: external_exports.unknown().optional()
23032
+ }).passthrough();
23033
+ function validateTaskPayload(task, label) {
23034
+ const fail = (message) => label ? `${label}: ${message}` : message;
23035
+ if (task.scopeId === void 0 !== (task.scopeRevision === void 0)) {
23036
+ return fail("scopeId and scopeRevision must be supplied together.");
23037
+ }
23038
+ if (task.topLogprobs !== void 0 && task.logprobs !== true) return fail("`topLogprobs` requires `logprobs:true`.");
23039
+ if (task.task === "embedding") {
23040
+ if (task.input === void 0) return fail('task "embedding" requires `input`.');
23041
+ const chatControls = [
23042
+ task.prompt,
23043
+ task.systemPrompt,
23044
+ task.images,
23045
+ task.messages,
23046
+ task.tools,
23047
+ task.format,
23048
+ task.think,
23049
+ task.logprobs,
23050
+ task.topLogprobs
23051
+ ];
23052
+ if (chatControls.some((value) => value !== void 0)) {
23053
+ return fail('task "embedding" accepts `input`, `options`, and routing fields; chat controls are not valid.');
23054
+ }
23055
+ return null;
23056
+ }
23057
+ if (task.input !== void 0 || task.returnEmbeddings !== void 0) {
23058
+ return fail('`input` and `returnEmbeddings` are valid only for task "embedding".');
23059
+ }
23060
+ if (task.prompt === void 0 && task.systemPrompt === void 0 && (task.messages === void 0 || task.messages.length === 0)) {
23061
+ return fail(`task "${task.task ?? "completion"}" requires \`prompt\`, \`systemPrompt\`, or \`messages\`.`);
23062
+ }
23063
+ if (task.images !== void 0 && (task.messages !== void 0 || task.prompt === void 0)) {
23064
+ return fail("Top-level `images` is valid only with `prompt`; put images inside messages instead.");
23065
+ }
23066
+ return null;
23067
+ }
23068
+ async function runBatch(tasks, maxParallelism, endpoint) {
23069
+ for (const item of tasks) {
23070
+ const problem = validateTaskPayload(item.task, `batch item ${item.id}`);
23071
+ if (problem) return errorResult(new Error(problem));
23072
+ }
23073
+ return withBatchTelemetry(parsedResult(await runTaskBatchRequest2(endpoint ?? DEFAULT_SERVE_ENDPOINT, {
23074
+ tasks: tasks.map(({ id, independent, task }) => ({
23075
+ id,
23076
+ independent,
23077
+ task: {
23078
+ task: task.task === void 0 ? task.scopeId === void 0 ? "completion" : void 0 : canonicalTaskKind(task.task),
23079
+ objective: task.objective ?? (task.scopeId === void 0 ? "balanced" : void 0),
23080
+ model: task.model,
23081
+ session_id: task.sessionId,
23082
+ scope_id: task.scopeId,
23083
+ scope_revision: task.scopeRevision,
23084
+ context_tokens: task.contextTokens,
23085
+ execution_preference: task.executionPreference,
23086
+ min_placement_evidence: task.minPlacementEvidence,
23087
+ required_capabilities: task.requiredCapabilities,
23088
+ priority: task.priority ?? "normal",
23089
+ max_wait_seconds: task.maxWaitSeconds,
23090
+ timeout_seconds: task.timeoutSeconds,
23091
+ stop_on_repetition: task.stopOnRepetition,
23092
+ prompt: task.prompt,
23093
+ images: task.images,
23094
+ messages: taskMessages(task),
23095
+ input: task.input,
23096
+ tools: task.tools,
23097
+ keep_alive: task.keepAlive,
23098
+ min_confidence: task.minConfidence,
23099
+ request_options: {
23100
+ format: task.format,
23101
+ think: task.think,
23102
+ options: task.options,
23103
+ logprobs: task.logprobs,
23104
+ top_logprobs: task.topLogprobs
23105
+ }
23106
+ }
23107
+ })),
23108
+ max_parallelism: maxParallelism
23109
+ })));
23110
+ }
23111
+ function registerRunTaskTool(server2) {
23112
+ server2.registerTool(
23113
+ "run_task",
23114
+ {
23115
+ description: "Use when: offloading caller-supplied work (chat, small tasks, code, tool calls, OCR/vision, embeddings) to a local model. Modes: single (`prompt`/`messages`, or `input` for embedding); `tasks` = up to 64 independent items, ordered results, `maxParallelism` caps dispatch; `preview:true` decides only; `warm:true` preloads `model`. Images: base64 `images` + the model's own prompt; `stopOnRepetition` ends a looping answer early, keeping its text. Do not use when: it needs file access (pass content, or delegate_research) or high-stakes judgment. Returns: answer text; receipts in structuredContent.",
23116
+ inputSchema: external_exports.object({
23117
+ endpoint: endpointParam,
23118
+ task: taskParam.removeDefault().optional().describe("caller owns prompts and output format."),
23119
+ objective: objectiveParam,
23120
+ model: external_exports.string().min(1).optional(),
23121
+ sessionId: external_exports.string().uuid().optional(),
23122
+ scopeId: external_exports.string().uuid().optional(),
23123
+ scopeRevision: external_exports.number().int().nonnegative().optional(),
23124
+ contextTokens: external_exports.number().int().positive().optional().describe("Total input + output window (num_ctx)."),
23125
+ executionPreference: executionPreferenceParam,
23126
+ minPlacementEvidence: minPlacementEvidenceParam,
23127
+ requiredCapabilities: requiredCapabilitiesParam,
23128
+ prompt: external_exports.string().min(1).optional(),
23129
+ systemPrompt: systemPromptParam,
23130
+ images: external_exports.array(external_exports.string().min(1)).min(1).optional().describe("base64, no data-URI prefix; prompt mode only; needs `model` or [policies.vision]"),
23131
+ messages: messagesParam.describe(
23132
+ "Caller system prompts; no injected task instructions. Overrides prompt; extra fields preserved."
23133
+ ),
23134
+ input: external_exports.union([external_exports.string().min(1), external_exports.array(external_exports.string().min(1)).min(1)]).optional(),
23135
+ tools: localToolsParam,
23136
+ keepAlive: external_exports.string().min(1).optional().describe('"0" unloads now, "-1" pins; omitted = adaptive finite TTL'),
23137
+ format: external_exports.union([external_exports.literal("json"), external_exports.record(external_exports.unknown())]).optional(),
23138
+ think: external_exports.union([external_exports.boolean(), external_exports.enum(["low", "medium", "high"])]).optional(),
23139
+ options: external_exports.record(external_exports.unknown()).optional().describe("num_ctx/contextTokens and num_gpu/placement are routing-owned; num_predict caps output."),
23140
+ logprobs: external_exports.boolean().optional(),
23141
+ topLogprobs: external_exports.number().int().nonnegative().optional(),
23142
+ minConfidence: minConfidenceParam,
23143
+ priority: external_exports.enum(["interactive", "normal", "background"]).optional(),
23144
+ maxWaitSeconds: external_exports.number().int().positive().optional(),
23145
+ timeoutSeconds: external_exports.number().int().positive().optional(),
23146
+ stopOnRepetition: external_exports.boolean().optional().describe("Stream and stop once output repeats itself (a loop to num_predict); keeps all generated text"),
23147
+ defer: external_exports.boolean().optional(),
23148
+ returnEmbeddings: external_exports.boolean().optional(),
23149
+ preview: external_exports.boolean().optional().describe(
23150
+ "true = routing fields only; rejects payloads and runtime controls; never executes"
23151
+ ),
23152
+ warm: external_exports.boolean().optional().describe("Load exact `model`; no prompt"),
23153
+ tasks: external_exports.array(batchItemParam).min(1).max(64).optional().describe("Batch mode: independent items; per-item fields go inside each item's `task`"),
23154
+ maxParallelism: external_exports.number().int().positive().max(64).optional().describe("Batch mode only")
23155
+ }).strict(),
23156
+ outputSchema: taskResultSchema.merge(batchResultSchema),
23157
+ annotations: { destructiveHint: false }
23158
+ },
23159
+ async ({
23160
+ endpoint,
23161
+ task,
23162
+ objective,
23163
+ model,
23164
+ sessionId,
23165
+ scopeId,
23166
+ scopeRevision,
23167
+ contextTokens,
23168
+ executionPreference,
23169
+ minPlacementEvidence,
23170
+ requiredCapabilities,
23171
+ prompt,
23172
+ systemPrompt,
23173
+ images,
23174
+ messages,
23175
+ input,
23176
+ tools,
23177
+ keepAlive,
23178
+ format,
23179
+ think,
23180
+ options,
23181
+ logprobs,
23182
+ topLogprobs,
23183
+ minConfidence,
23184
+ priority,
23185
+ maxWaitSeconds,
23186
+ timeoutSeconds,
23187
+ stopOnRepetition,
23188
+ defer,
23189
+ returnEmbeddings,
23190
+ preview,
23191
+ warm,
23192
+ tasks,
23193
+ maxParallelism
23194
+ }) => {
23195
+ try {
23196
+ const singleFields = {
23197
+ task,
23198
+ objective,
23199
+ model,
23200
+ sessionId,
23201
+ scopeId,
23202
+ scopeRevision,
23203
+ contextTokens,
23204
+ executionPreference,
23205
+ minPlacementEvidence,
23206
+ requiredCapabilities,
23207
+ prompt,
23208
+ systemPrompt,
23209
+ images,
23210
+ messages,
23211
+ input,
23212
+ tools,
23213
+ keepAlive,
23214
+ format,
23215
+ think,
23216
+ options,
23217
+ logprobs,
23218
+ topLogprobs,
23219
+ minConfidence,
23220
+ priority,
23221
+ maxWaitSeconds,
23222
+ timeoutSeconds,
23223
+ stopOnRepetition,
23224
+ defer,
23225
+ returnEmbeddings,
23226
+ preview,
23227
+ warm
23228
+ };
23229
+ if (tasks !== void 0) {
23230
+ const stray = Object.entries(singleFields).filter(([, value]) => value !== void 0).map(([name]) => name);
23231
+ const unsupported = stray.filter((name) => !BATCH_TASK_FIELDS.has(name)).map((name) => `\`${name}\``);
23232
+ if (unsupported.length > 0) {
23233
+ return errorResult(new Error(`${unsupported.join(", ")} ${unsupported.length === 1 ? "is" : "are"} not supported with \`tasks\`.`));
23234
+ }
23235
+ const movable = stray.map((name) => `\`${name}\``);
23236
+ if (movable.length > 0) {
23237
+ return errorResult(new Error(`With \`tasks\`, put ${movable.join(", ")} inside each item's \`task\`.`));
23238
+ }
23239
+ return await runBatch(tasks, maxParallelism, endpoint);
23240
+ }
23241
+ if (maxParallelism !== void 0) return errorResult(new Error("`maxParallelism` is valid only with `tasks`."));
23242
+ if (warm) {
23243
+ const allowed = /* @__PURE__ */ new Set([
23244
+ "warm",
23245
+ "model",
23246
+ "task",
23247
+ "contextTokens",
23248
+ "executionPreference",
23249
+ "minPlacementEvidence",
23250
+ "priority",
23251
+ "keepAlive",
23252
+ "maxWaitSeconds",
23253
+ "timeoutSeconds",
23254
+ "defer"
23255
+ ]);
23256
+ const stray = Object.entries(singleFields).filter(([name, value]) => value !== void 0 && !allowed.has(name)).map(([name]) => `\`${name}\``);
23257
+ if (stray.length > 0) return errorResult(new Error(`\`warm:true\` loads a model without generation; remove ${stray.join(", ")}.`));
23258
+ if (model === void 0) return errorResult(new Error("`warm:true` requires an exact installed `model`."));
23259
+ if (task === "embedding") return errorResult(new Error("`warm:true` supports chat models; embedding warming is unsupported."));
23260
+ return parsedResult(await warmModelRequest2(endpoint, {
23261
+ model,
23262
+ task: task === void 0 ? void 0 : canonicalTaskKind(task),
23263
+ context_tokens: contextTokens,
23264
+ execution_preference: executionPreference,
23265
+ min_placement_evidence: minPlacementEvidence,
23266
+ priority,
23267
+ keep_alive: keepAlive,
23268
+ max_wait_seconds: maxWaitSeconds,
23269
+ timeout_seconds: timeoutSeconds,
23270
+ defer
23271
+ }));
23272
+ }
23273
+ const canonicalTask = canonicalTaskKind(task ?? "completion");
23274
+ if (preview) {
23275
+ const executionOnlyFields = [
23276
+ ["scopeId", scopeId],
23277
+ ["scopeRevision", scopeRevision],
23278
+ ["prompt", prompt],
23279
+ ["systemPrompt", systemPrompt],
23280
+ ["images", images],
23281
+ ["messages", messages],
23282
+ ["input", input],
23283
+ ["tools", tools],
23284
+ ["keepAlive", keepAlive],
23285
+ ["format", format],
23286
+ ["think", think],
23287
+ ["options", options],
23288
+ ["logprobs", logprobs],
23289
+ ["topLogprobs", topLogprobs],
23290
+ ["returnEmbeddings", returnEmbeddings],
23291
+ ["maxWaitSeconds", maxWaitSeconds],
23292
+ ["priority", priority],
23293
+ ["timeoutSeconds", timeoutSeconds],
23294
+ ["stopOnRepetition", stopOnRepetition],
23295
+ ["defer", defer]
23296
+ ].filter(([, value]) => value !== void 0).map(([name]) => `\`${name}\``);
23297
+ if (executionOnlyFields.length > 0) {
23298
+ return errorResult(
23299
+ new Error(
23300
+ `\`preview:true\` accepts routing fields only; remove execution-only fields ${executionOnlyFields.join(", ")}. Use \`requiredCapabilities:["tools"]\` to preview tool capability, then make a separate execution call with the payload.`
23301
+ )
23302
+ );
23303
+ }
23304
+ }
23305
+ const effectiveRequiredCapabilities = tools === void 0 ? requiredCapabilities : withRequiredCapability(requiredCapabilities, "tools");
23306
+ if (preview) {
23307
+ return parsedResult(
23308
+ await route2(endpoint, canonicalTask, objective, model, sessionId, contextTokens, effectiveRequiredCapabilities, minConfidence, executionPreference, minPlacementEvidence)
23309
+ );
23310
+ }
23311
+ const problem = validateTaskPayload({
23312
+ task,
23313
+ scopeId,
23314
+ scopeRevision,
23315
+ prompt,
23316
+ systemPrompt,
23317
+ messages,
23318
+ images,
23319
+ input,
23320
+ tools,
23321
+ format,
23322
+ think,
23323
+ logprobs,
23324
+ topLogprobs,
23325
+ returnEmbeddings
23326
+ });
23327
+ if (problem) return errorResult(new Error(problem));
23328
+ if (minConfidence && defer && scopeId === void 0) {
23329
+ const decision = parsedResult(
23330
+ await route2(endpoint, canonicalTask, objective, model, sessionId, contextTokens, effectiveRequiredCapabilities, minConfidence, executionPreference, minPlacementEvidence)
23331
+ );
23332
+ if (!isStructuredSuccess(decision)) return decision;
23333
+ }
23334
+ const result = parsedResult(
23335
+ await runTaskRequest2(endpoint ?? DEFAULT_SERVE_ENDPOINT, {
23336
+ task: scopeId !== void 0 && task === void 0 ? void 0 : canonicalTask,
23337
+ objective: objective ?? (scopeId === void 0 ? "balanced" : void 0),
23338
+ model,
23339
+ session_id: sessionId,
23340
+ scope_id: scopeId,
23341
+ scope_revision: scopeRevision,
23342
+ context_tokens: contextTokens,
23343
+ required_capabilities: effectiveRequiredCapabilities,
23344
+ prompt,
23345
+ images,
23346
+ messages: taskMessages({ systemPrompt, messages, prompt, images }),
23347
+ input,
23348
+ tools,
23349
+ keep_alive: keepAlive,
23350
+ min_confidence: minConfidence,
23351
+ priority: priority ?? "normal",
23352
+ max_wait_seconds: maxWaitSeconds,
23353
+ timeout_seconds: timeoutSeconds,
23354
+ stop_on_repetition: stopOnRepetition,
23355
+ defer: defer ?? false,
23356
+ execution_preference: executionPreference,
23357
+ min_placement_evidence: minPlacementEvidence,
23358
+ request_options: {
23359
+ format,
23360
+ think,
23361
+ options,
23362
+ logprobs,
23363
+ top_logprobs: topLogprobs
23364
+ }
23365
+ })
23366
+ );
23367
+ if (isStructuredSuccess(result) && result.structuredContent.deferred === true) return result;
23368
+ if (!returnEmbeddings && isStructuredSuccess(result)) {
23369
+ const trimmed = summarizeEmbeddings(result.structuredContent);
23370
+ if (trimmed) return withTaskTelemetry(structuredResult(trimmed));
23371
+ }
23372
+ return withTaskTelemetry(result);
23373
+ } catch (error2) {
23374
+ return errorResult(error2);
23375
+ }
23376
+ }
23377
+ );
23378
+ }
23379
+
23380
+ // src/tools/task-jobs.ts
23381
+ function registerTaskJobsTool(server2) {
23382
+ server2.registerTool(
23383
+ "task_jobs",
23384
+ {
23385
+ description: "Use when: following work submitted with run_task {defer:true}: list, get (result), cancel, or remove a job. Do not use when: submitting new work or killing a whole session. Returns: job status/result; remove cancels first.",
23386
+ inputSchema: external_exports.object({
23387
+ endpoint: endpointParam,
23388
+ action: external_exports.enum(["list", "get", "cancel", "remove"]),
23389
+ jobId: external_exports.string().uuid().optional(),
23390
+ returnEmbeddings: external_exports.boolean().optional()
23391
+ }).strict(),
23392
+ outputSchema: objectResultSchema,
23393
+ annotations: { destructiveHint: true }
23394
+ },
23395
+ async ({ endpoint, action, jobId, returnEmbeddings }) => {
23396
+ try {
23397
+ if (action === "list" ? jobId !== void 0 : jobId === void 0) {
23398
+ return errorResult(new Error("Use jobId only with get/cancel/remove; all require a jobId."));
23399
+ }
23400
+ if (returnEmbeddings !== void 0 && action !== "get") {
23401
+ return errorResult(new Error("returnEmbeddings is valid only with get."));
23402
+ }
23403
+ const result = parsedResult(action === "list" ? await listTaskJobs2(endpoint) : action === "get" ? await getTaskJob2(endpoint, jobId) : action === "cancel" ? await cancelTaskJob2(endpoint, jobId) : await removeTaskJob2(endpoint, jobId));
23404
+ if (!returnEmbeddings && isStructuredSuccess(result)) {
23405
+ const job = result.structuredContent.job;
23406
+ if (job?.result && typeof job.result === "object") {
23407
+ const trimmed = summarizeEmbeddings(job.result);
23408
+ if (trimmed) return structuredResult({ ...result.structuredContent, job: { ...job, result: trimmed } });
23409
+ }
23410
+ }
23411
+ return result;
23412
+ } catch (error2) {
23413
+ return errorResult(error2);
23414
+ }
23415
+ }
23416
+ );
23417
+ }
23418
+
23419
+ // src/tools/ollama-lifecycle.ts
23420
+ var manageResultSchema = external_exports.object({
23421
+ status: external_exports.string().optional(),
23422
+ progress: external_exports.unknown().optional()
23423
+ }).passthrough();
23424
+ var deleteResultSchema = external_exports.object({
23425
+ deleted: external_exports.string()
23426
+ }).passthrough();
23427
+ function summarizeOllamaPullStream(text) {
23428
+ const events = [];
23429
+ for (const line of text.split(/\r?\n/)) {
23430
+ const trimmed = line.trim();
23431
+ if (!trimmed) continue;
22816
23432
  try {
22817
- const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
22818
- if (!Number.isInteger(parsed.offset) || parsed.offset < 0 || parsed.fingerprint !== fingerprint) {
22819
- throw new Error("invalid or stale cursor");
23433
+ const parsed = JSON.parse(trimmed);
23434
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
23435
+ events.push(parsed);
22820
23436
  }
22821
- offset = parsed.offset;
22822
23437
  } catch {
22823
- throw new Error("`cursor` is invalid or the model list changed; restart without cursor.");
22824
23438
  }
22825
23439
  }
22826
- const page = items.slice(offset, offset + pageSize);
22827
- const nextOffset = offset + page.length;
22828
- return {
22829
- items: page,
22830
- returned: page.length,
22831
- total: items.length,
22832
- next_cursor: nextOffset < items.length ? Buffer.from(JSON.stringify({ offset: nextOffset, fingerprint }), "utf8").toString("base64url") : null
22833
- };
22834
- }
22835
- var liveDelegates = /* @__PURE__ */ new Set();
22836
- function killLiveDelegates() {
22837
- for (const child2 of liveDelegates) {
23440
+ if (events.length === 0) {
23441
+ if (!text.trim()) return { status: "empty" };
22838
23442
  try {
22839
- child2.kill("SIGKILL");
23443
+ const parsed = JSON.parse(text);
23444
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
23445
+ return parsed;
23446
+ }
22840
23447
  } catch {
23448
+ return { status: "unparsed", raw: clipText(text, 500) };
22841
23449
  }
23450
+ return { status: "unparsed", raw: clipText(text, 500) };
22842
23451
  }
22843
- liveDelegates.clear();
23452
+ const last = events[events.length - 1] ?? {};
23453
+ const withBytes = [...events].reverse().find((event) => typeof event.total === "number" && event.total > 0);
23454
+ const completed = typeof withBytes?.completed === "number" ? withBytes.completed : null;
23455
+ const total = typeof withBytes?.total === "number" ? withBytes.total : null;
23456
+ const percent = completed != null && total != null && total > 0 ? Math.round(completed / total * 1e3) / 10 : null;
23457
+ const status2 = typeof last.error === "string" && last.error ? "error" : last.status;
23458
+ return {
23459
+ ...last,
23460
+ ...status2 === void 0 ? {} : { status: status2 },
23461
+ progress: {
23462
+ events: events.length,
23463
+ completed,
23464
+ total,
23465
+ percent,
23466
+ lastStatus: last.status ?? null
23467
+ }
23468
+ };
22844
23469
  }
22845
- for (const stream of [process.stdout, process.stderr]) {
22846
- stream.on("error", (error2) => {
22847
- if (error2.code === "EPIPE") process.exit(0);
22848
- throw error2;
23470
+ async function ollamaPull(endpoint, model, timeoutSeconds, options = {}) {
23471
+ const raw = await ollamaFetch(endpoint, "/api/pull", {
23472
+ method: "POST",
23473
+ body: { model, stream: true },
23474
+ timeoutMs: (timeoutSeconds ?? DEFAULT_PULL_TIMEOUT_SECONDS) * 1e3,
23475
+ parse: "ndjson",
23476
+ signal: options.signal,
23477
+ onLine: options.onProgress
22849
23478
  });
23479
+ if (typeof raw === "string") return summarizeOllamaPullStream(raw);
23480
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) return raw;
23481
+ return { status: "empty" };
22850
23482
  }
22851
- process.on("exit", () => {
22852
- killLiveDelegates();
22853
- stopAutostartedServe();
22854
- });
22855
- process.stdin.on("close", () => {
22856
- killLiveDelegates();
22857
- stopAutostartedServe();
22858
- process.exit(0);
22859
- });
22860
23483
  function pullProgressReporter(extra) {
22861
23484
  const token = extra._meta?.progressToken;
22862
23485
  if (token === void 0) return void 0;
@@ -22880,769 +23503,245 @@ function pullProgressReporter(extra) {
22880
23503
  }).catch(() => void 0);
22881
23504
  };
22882
23505
  }
22883
- for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
22884
- process.on(signal, () => {
22885
- killLiveDelegates();
22886
- process.exit(0);
22887
- });
22888
- }
22889
- var INSTRUCTIONS = `models{view:"installed"}; models{view:"resident"} for residency.
22890
- run_task preview never executes; code_review aliases coding. Preview routing only; requiredCapabilities:["tools"] previews tools; omit preview and supply the payload. Caller executes tools.
22891
- quality needs policy/model; medium needs policy+benchmark; observed needs /api/ps proof.
22892
- Scoped run_task needs scopeId+scopeRevision. Scopes store history; sessions affinity, not messages/KV. defer:true: job.id\u2192task_jobs.jobId unchanged. Restart loses scopes/sessions/jobs.
22893
- Check isError first; prefer structuredContent, else content[].text. page.next_cursor\u2192cursor unchanged.
22894
- caller owns task decomposition/prompts/format/verification; findings are candidates, not accepted defects. operator owns endpoints, exact --cpu-model assignments/lifecycle. Ollama plus the OS/driver run physical CPU/GPU.
22895
- ask approval for one exact tag and reported size before ollama_manage pull; search or recommendation is never download permission. Stop/delete need exact-tag approval.
22896
- serve :11435; default-endpoint autostart optional. Docs: freellama://docs/index.`;
22897
- var server = new McpServer(
22898
- { name: "freellama", version: SERVER_VERSION },
22899
- { instructions: INSTRUCTIONS }
22900
- );
22901
- var PACKAGED_DOCS_DIR = path5.resolve(path5.dirname(fileURLToPath3(import.meta.url)), "..", "docs");
22902
- if (!existsSync5(PACKAGED_DOCS_DIR)) {
22903
- throw new Error(`FreeLlama MCP documentation is missing at ${PACKAGED_DOCS_DIR}; run the package build.`);
22904
- }
22905
- var PACKAGED_DEV_DOCS_DIR = path5.join(PACKAGED_DOCS_DIR, "dev");
22906
- var packagedDocs = [
22907
- ...readdirSync(PACKAGED_DOCS_DIR).filter((name) => name.endsWith(".md")).sort(),
22908
- ...existsSync5(PACKAGED_DEV_DOCS_DIR) ? readdirSync(PACKAGED_DEV_DOCS_DIR).filter((name) => name.endsWith(".md")).sort().map((name) => `dev/${name}`) : []
22909
- ];
22910
- var packagedDocNames = packagedDocs.map((name) => path5.basename(name));
22911
- if (new Set(packagedDocNames).size !== packagedDocNames.length) {
22912
- throw new Error(`FreeLlama MCP documentation has duplicate names across docs/ and docs/dev/: ${packagedDocs.join(", ")}`);
22913
- }
22914
- if (!packagedDocs.includes("INDEX.md")) {
22915
- throw new Error(`FreeLlama MCP documentation index is missing at ${PACKAGED_DOCS_DIR}/INDEX.md; run the package build.`);
22916
- }
22917
- for (const name of packagedDocs) {
22918
- const base = path5.basename(name);
22919
- const uri = `freellama://docs/${base === "INDEX.md" ? "index" : base.replace(/\.md$/, "")}`;
22920
- server.registerResource(
22921
- `freellama-docs-${base.toLowerCase().replace(/\.md$/, "")}`,
22922
- uri,
23506
+ function registerOllamaLifecycleTools(server2) {
23507
+ server2.registerTool(
23508
+ "ollama_manage",
22923
23509
  {
22924
- mimeType: "text/markdown",
22925
- description: name === "INDEX.md" ? "Index of packaged FreeLlama operator and agent documentation. Read this first, then fetch one relevant document." : `Packaged FreeLlama documentation: ${name}.`
23510
+ description: "Use when: a human approved pulling one exact tag (action:pull) or unloading a loaded model (action:stop). Do not use when: deleting (ollama_delete), searching (models), or preloading (run_task {warm:true}). Returns: receipt; timeoutSeconds is pull-only.",
23511
+ inputSchema: external_exports.object({
23512
+ action: external_exports.enum(["pull", "stop"]),
23513
+ model: external_exports.string().min(1),
23514
+ ollamaEndpoint: ollamaEndpointParam,
23515
+ timeoutSeconds: external_exports.number().int().positive().optional()
23516
+ }).strict(),
23517
+ outputSchema: manageResultSchema,
23518
+ annotations: { destructiveHint: false }
22926
23519
  },
22927
- async () => ({
22928
- contents: [{ uri, mimeType: "text/markdown", text: await readFile(path5.join(PACKAGED_DOCS_DIR, name), "utf8") }]
22929
- })
22930
- );
22931
- }
22932
- server.registerTool(
22933
- "doctor",
22934
- {
22935
- description: "Use when: diagnosis/status/usage. Do not use when: choosing models. Returns: summary; opt into detail.",
22936
- inputSchema: external_exports.object({
22937
- endpoint: ollamaEndpointParam,
22938
- serveEndpoint: endpointParam,
22939
- view: external_exports.enum(["summary", "scheduler", "config", "full", "status", "usage"]).optional(),
22940
- days: external_exports.number().int().min(1).max(366).optional()
22941
- }).strict(),
22942
- outputSchema: doctorResultSchema,
22943
- annotations: { readOnlyHint: true }
22944
- },
22945
- async ({ endpoint, serveEndpoint, view, days }) => {
22946
- try {
22947
- if (days !== void 0 && view !== "usage") return errorResult(new Error("days is only valid with view: usage."));
22948
- if (view === "status") return parsedResult(await status(serveEndpoint));
22949
- if (view === "usage") return parsedResult(await usage(serveEndpoint, days));
22950
- const report = parsedResult(await doctor(endpoint));
22951
- if (!isStructuredSuccess(report)) return report;
23520
+ async ({ action, model, ollamaEndpoint, timeoutSeconds }, extra) => {
22952
23521
  try {
22953
- report.structuredContent.machine = JSON.parse(await machine(serveEndpoint));
22954
- delete report.structuredContent.machine_unavailable;
22955
- } catch (error2) {
22956
- if (report.structuredContent.machine == null) {
22957
- report.structuredContent.machine_unavailable = `freellama serve unreachable, so no machine profile: ${error2 instanceof Error ? error2.message : String(error2)}`;
23522
+ if (action === "stop" && timeoutSeconds !== void 0) {
23523
+ return errorResult(new Error('`timeoutSeconds` is valid only for action "pull".'));
23524
+ }
23525
+ const data = action === "pull" ? await ollamaPull(ollamaEndpoint, model, timeoutSeconds, {
23526
+ signal: extra.signal,
23527
+ onProgress: pullProgressReporter(extra)
23528
+ }) : await ollamaFetch(ollamaEndpoint, "/api/generate", {
23529
+ method: "POST",
23530
+ body: { model, keep_alive: 0 },
23531
+ signal: extra.signal
23532
+ });
23533
+ const payload = data;
23534
+ if (typeof payload.error === "string" && payload.error) {
23535
+ return errorResult(new Error(`${action} ${model} failed: ${payload.error}`));
22958
23536
  }
22959
- }
22960
- try {
22961
- report.structuredContent.platform_health = JSON.parse(await health(serveEndpoint));
23537
+ return structuredResult(payload);
22962
23538
  } catch (error2) {
22963
- report.structuredContent.platform_health_unavailable = `freellama serve unreachable: ${error2 instanceof Error ? error2.message : String(error2)}`;
23539
+ return errorResult(error2);
22964
23540
  }
22965
- const full = { ...report.structuredContent };
22966
- const flatConfig = full.ollama_env_config;
22967
- delete full.ollama_env_config;
22968
- if (view === "config") {
22969
- return structuredResult({
22970
- status: "ok",
22971
- summary: "Categorized Ollama configuration; values are source-qualified, not endpoint/PID proof.",
22972
- endpoint: full.endpoint,
22973
- ollama_config: full.ollama_config,
22974
- ollama_env_config_source: full.ollama_env_config_source,
22975
- ollama_env_config_warning: full.ollama_env_config_warning,
22976
- host_runtime_signals: full.host_runtime_signals
23541
+ }
23542
+ );
23543
+ server2.registerTool(
23544
+ "ollama_delete",
23545
+ {
23546
+ description: "DESTRUCTIVE AND IRREVERSIBLE. Use when: a human explicitly names the exact installed tag to delete. Do not use when: you inferred the cleanup yourself, or only want to unload (ollama_manage stop). Returns: the deleted tag.",
23547
+ inputSchema: external_exports.object({
23548
+ model: external_exports.string().min(1),
23549
+ ollamaEndpoint: ollamaEndpointParam
23550
+ }).strict(),
23551
+ outputSchema: deleteResultSchema,
23552
+ annotations: { destructiveHint: true }
23553
+ },
23554
+ async ({ model, ollamaEndpoint }) => {
23555
+ try {
23556
+ await ollamaFetch(ollamaEndpoint, "/api/delete", {
23557
+ method: "DELETE",
23558
+ body: { model }
22977
23559
  });
23560
+ return structuredResult({ deleted: model });
23561
+ } catch (error2) {
23562
+ return errorResult(error2);
22978
23563
  }
22979
- if ((view ?? "summary") === "full") return structuredResult(full);
22980
- const running = full.running?.models ?? [];
22981
- const platformHealth = full.platform_health;
22982
- const compact = {
22983
- status: "ok",
22984
- summary: `Ollama ${String(full.version?.version ?? "unknown")}; ${running.length} resident model(s).`,
22985
- endpoint: full.endpoint,
22986
- ollama: { endpoint: full.endpoint, version: full.version, resident_model_count: running.length },
22987
- machine: full.machine,
22988
- local_conservative_config_posture: full.local_conservative_config_posture,
22989
- host_runtime_signals: full.host_runtime_signals,
22990
- ...view === "scheduler" ? {
22991
- scheduler: {
22992
- admission: platformHealth?.admission ?? null,
22993
- ollama_num_parallel: flatConfig?.OLLAMA_NUM_PARALLEL,
22994
- ollama_max_queue: flatConfig?.OLLAMA_MAX_QUEUE,
22995
- proof_level: "configured_and_snapshot_only; measure concurrent execution before claiming throughput"
22996
- }
22997
- } : {},
22998
- evidence: { runtime: "ollama_api_version_and_ps", configuration: full.ollama_env_config_source },
22999
- next: view === "scheduler" ? "Use run_task preview for a per-task advisory receipt." : 'Use models{view:"installed"} to select a local model.'
23000
- };
23001
- return structuredResult(compact);
23002
- } catch (error2) {
23003
- return errorResult(error2);
23004
23564
  }
23005
- }
23006
- );
23007
- server.registerTool(
23008
- "session",
23009
- {
23010
- description: "Use when: affinity/session kill. Do not use when: job cancel/history/unload. Returns: receipt.",
23011
- inputSchema: external_exports.object({
23012
- action: external_exports.enum(["create", "delete", "kill"]),
23013
- sessionId: external_exports.string().uuid().optional(),
23014
- endpoint: endpointParam
23015
- }).strict(),
23016
- outputSchema: sessionResultSchema,
23017
- annotations: { destructiveHint: true }
23018
- },
23019
- async ({ action, sessionId, endpoint }) => {
23020
- try {
23021
- if (action === "create") {
23022
- if (sessionId !== void 0) return errorResult(new Error("sessionId is only valid for action: delete or kill."));
23023
- return structuredResult(JSON.parse(await createSession2(endpoint)));
23024
- }
23025
- if (sessionId === void 0) return errorResult(new Error(`action: ${action} requires sessionId.`));
23026
- if (action === "kill") return parsedResult(await killSession2(endpoint, sessionId));
23027
- await deleteSession2(endpoint, sessionId);
23028
- return structuredResult({ session_id: sessionId, deleted: true });
23029
- } catch (error2) {
23030
- return errorResult(error2);
23031
- }
23032
- }
23033
- );
23034
- server.registerTool(
23035
- "models",
23036
- {
23037
- description: "Use when: inventory/library. Do not use when: execution/mutation. Returns: models; library finds families, then one family's tags.",
23038
- inputSchema: external_exports.object({
23039
- view: external_exports.enum(["installed", "resident", "detail", "raw", "library"]).optional(),
23040
- model: external_exports.string().min(1).optional(),
23041
- includeVerbose: external_exports.boolean().optional(),
23042
- includeLibrary: external_exports.boolean().optional(),
23043
- includeReadme: external_exports.boolean().optional(),
23044
- query: external_exports.string().min(1).optional(),
23045
- capabilities: external_exports.array(external_exports.enum(["vision", "tools", "thinking", "embedding", "cloud"])).min(1).max(5).optional(),
23046
- order: external_exports.enum(["popular", "newest"]).optional(),
23047
- limit: external_exports.number().int().positive().max(50).optional(),
23048
- cursor: external_exports.string().min(1).optional(),
23049
- endpoint: endpointParam,
23050
- ollamaEndpoint: ollamaEndpointParam
23051
- }).strict(),
23052
- outputSchema: modelsResultSchema,
23053
- annotations: { readOnlyHint: true }
23054
- },
23055
- async ({ view, model, includeVerbose, includeLibrary, includeReadme, query, capabilities, order, limit, cursor, endpoint, ollamaEndpoint }) => {
23056
- try {
23057
- const selectedView = view ?? "installed";
23058
- if (includeLibrary !== void 0 && selectedView !== "installed" && selectedView !== "detail") {
23059
- return errorResult(new Error('`includeLibrary` is valid only for views "installed" and "detail".'));
23060
- }
23061
- if (includeReadme !== void 0 && !(selectedView === "library" && model || selectedView === "detail" && includeLibrary === true)) {
23062
- return errorResult(new Error('`includeReadme` needs library step 2, or view "detail" with `includeLibrary:true`.'));
23063
- }
23064
- if (selectedView !== "library" && [query, capabilities, order].some((value) => value !== void 0)) {
23065
- return errorResult(new Error(`view "${selectedView}" does not accept library search fields.`));
23066
- }
23067
- if (selectedView !== "library" && selectedView !== "raw" && [limit, cursor].some((value) => value !== void 0)) {
23068
- return errorResult(new Error(`view "${selectedView}" accepts neither pagination nor library search fields.`));
23069
- }
23070
- if (selectedView !== "detail" && includeVerbose !== void 0) {
23071
- return errorResult(new Error('`includeVerbose` is valid only for view "detail".'));
23072
- }
23073
- if (selectedView !== "detail" && selectedView !== "library" && model !== void 0) {
23074
- return errorResult(new Error('`model` is valid only for views "detail" and "library".'));
23075
- }
23076
- if (selectedView === "library" && model && [query, capabilities, order].some((value) => value !== void 0)) {
23077
- return errorResult(
23078
- new Error("Library step 2 accepts only `model` plus endpoint overrides; omit step-1 search fields.")
23079
- );
23080
- }
23081
- switch (selectedView) {
23082
- case "raw": {
23083
- const raw = await ollamaFetch(ollamaEndpoint, "/api/tags");
23084
- const models = Array.isArray(raw.models) ? raw.models : [];
23085
- const page = pageLiveList(models, limit, cursor, (model2) => String(model2.name ?? JSON.stringify(model2)));
23086
- return structuredResult({ ...raw, models: page.items, page: { returned: page.returned, total: page.total, next_cursor: page.next_cursor } });
23087
- }
23088
- case "resident": {
23089
- const managed = parsedResult(await listModels2(endpoint));
23090
- if (!isStructuredSuccess(managed)) return managed;
23091
- const data = managed.structuredContent;
23092
- const models = (data.models ?? []).filter((entry) => entry.resident === true).map((entry) => {
23093
- const size = typeof entry.resident_size === "number" ? entry.resident_size : null;
23094
- const vram = typeof entry.resident_vram === "number" ? entry.resident_vram : null;
23095
- if (size === null || vram === null || size === 0) return entry;
23096
- const gpuPercent = Math.round(vram / size * 100);
23097
- const execution = entry.execution;
23098
- const assignedCpu = execution?.placement === "cpu";
23099
- const observedProcessor = execution?.observation?.processor;
23100
- const processor = observedProcessor === "cpu" || observedProcessor === "gpu" || observedProcessor === "mixed" ? observedProcessor : gpuPercent >= 100 ? "gpu" : gpuPercent <= 0 ? "cpu" : "mixed";
23101
- const mismatch = execution?.observation?.status === "mismatch";
23102
- return {
23103
- ...entry,
23104
- placement: {
23105
- gpu_percent: gpuPercent,
23106
- assigned: assignedCpu,
23107
- processor: processor === "gpu" ? "100% GPU" : processor === "cpu" ? "100% CPU" : `${gpuPercent}% GPU / ${100 - gpuPercent}% CPU`,
23108
- ...mismatch ? {
23109
- warning: `Configured ${execution?.placement ?? "unknown"} backend disagrees with Ollama /api/ps: ${processor} was physically observed. This sample is excluded from adaptive routing feedback.`
23110
- } : processor === "mixed" ? {
23111
- warning: 'Partially offloaded to CPU \u2014 expect a large slowdown. Free VRAM (`ollama_manage` action "stop") or lower the context length.'
23112
- } : {}
23113
- }
23114
- };
23115
- });
23116
- return structuredResult({ ...data, models });
23117
- }
23118
- case "detail": {
23119
- if (!model) {
23120
- return errorResult(new Error('view "detail" needs `model` set to an installed tag.'));
23121
- }
23122
- const data = await ollamaFetch(ollamaEndpoint, "/api/show", {
23123
- method: "POST",
23124
- body: { model }
23125
- });
23126
- let local = data;
23127
- if (includeLibrary && !data.remote_host && !data.remote_model && !/-cloud$/i.test(model)) {
23128
- try {
23129
- const inventory = await ollamaFetch(ollamaEndpoint, "/api/tags");
23130
- const canonical = libraryReference(model)?.tag;
23131
- const entry = canonical && inventory.models?.find((entry2) => libraryReference(String(entry2.name ?? entry2.model))?.tag === canonical);
23132
- if (entry) local = { ...entry, ...data, digest: entry.digest };
23133
- } catch {
23134
- }
23135
- }
23136
- const library = includeLibrary && !local.remote_host && !local.remote_model && !/-cloud$/i.test(model) ? await ollamaLibrary.lookup(model) : void 0;
23137
- const { license, modelfile, ...rest } = data;
23138
- const modelInfo = rest.model_info ?? {};
23139
- const contextEntry = Object.entries(modelInfo).find(
23140
- ([key, value]) => key.endsWith(".context_length") && typeof value === "number"
23141
- );
23142
- return structuredResult({
23143
- ...rest,
23144
- max_context_length: contextEntry?.[1] ?? null,
23145
- knowledge: modelKnowledge(model, local, library, includeReadme),
23146
- ...includeVerbose ? { license, modelfile } : {}
23147
- });
23148
- }
23149
- case "library":
23150
- return await libraryLookup({ model, query, capabilities, order, limit, cursor, includeReadme, endpoint, ollamaEndpoint });
23151
- default: {
23152
- const result = parsedResult(await listModels2(endpoint));
23153
- if (!isStructuredSuccess(result)) return result;
23154
- const data = result.structuredContent;
23155
- return structuredResult({ ...data, models: await enrichInstalledModels(
23156
- data.models ?? [],
23157
- ollamaLibrary,
23158
- includeLibrary ?? false,
23159
- async (upstream) => (await ollamaFetch(upstream ?? ollamaEndpoint, "/api/tags")).models ?? []
23160
- ) });
23161
- }
23565
+ );
23566
+ }
23567
+
23568
+ // src/tools/delegate-research.ts
23569
+ import { existsSync as existsSync5 } from "node:fs";
23570
+ import { mkdtemp, readFile as readFile2, rm, writeFile } from "node:fs/promises";
23571
+ import { tmpdir } from "node:os";
23572
+ import path5 from "node:path";
23573
+
23574
+ // src/delegate.ts
23575
+ import { existsSync as existsSync4, readFileSync, realpathSync, statSync } from "node:fs";
23576
+ import path4 from "node:path";
23577
+ function researchErrorResult(error2, diagnostic) {
23578
+ const result = errorResult(error2);
23579
+ const summary = diagnostic ?? result.content[0].text;
23580
+ return {
23581
+ ...result,
23582
+ content: [{ type: "text", text: summary }],
23583
+ structuredContent: {
23584
+ ...result.structuredContent ?? { error: summary },
23585
+ answer: "",
23586
+ summary,
23587
+ verification: {
23588
+ recommendation: "escalate",
23589
+ grounded: false,
23590
+ why: "The research request failed; no usable answer was produced."
23162
23591
  }
23163
- } catch (error2) {
23164
- return errorResult(error2);
23165
- }
23166
- }
23167
- );
23168
- async function libraryLookup({
23169
- model,
23170
- query,
23171
- capabilities,
23172
- order,
23173
- limit,
23174
- cursor,
23175
- includeReadme,
23176
- endpoint,
23177
- ollamaEndpoint
23178
- }) {
23179
- const localState = async () => {
23180
- let installed = /* @__PURE__ */ new Set();
23181
- let memoryBytes = null;
23182
- try {
23183
- const tags = await ollamaFetch(ollamaEndpoint, "/api/tags");
23184
- installed = new Set((tags.models ?? []).map((m) => m.name ?? ""));
23185
- } catch {
23186
- }
23187
- try {
23188
- const profile = JSON.parse(await machine(endpoint));
23189
- memoryBytes = profile.memory_bytes ?? profile.unified_memory_bytes ?? null;
23190
- } catch {
23191
23592
  }
23192
- return { installed, memoryBytes };
23193
23593
  };
23194
- try {
23195
- if (model) {
23196
- const ref = libraryReference(model);
23197
- if (!ref) throw new Error("Use an exact Ollama family or namespace/model name, not a URL or path.");
23198
- const { family } = ref;
23199
- const library = await ollamaLibrary.lookup(model);
23200
- if (library.status !== "available") return structuredResult({
23201
- family,
23202
- sourcePage: ref.url,
23203
- tags: [],
23204
- tagsUnavailable: library.message,
23205
- library,
23206
- recommendation: null
23207
- });
23208
- const tags = library.tags;
23209
- const { installed: installed2, memoryBytes } = await localState();
23210
- const budget = memoryBytes ? memoryBytes * 0.6 : null;
23211
- const annotated = tags.map((t) => ({
23212
- ...t,
23213
- installed: installed2.has(t.tag),
23214
- fitsInMemory: budget && t.sizeBytes ? t.sizeBytes <= budget : null,
23215
- fitScope: "host_memory_budget_only"
23216
- }));
23217
- const runnable = annotated.filter((t) => t.fitsInMemory === true && !t.cloud);
23218
- const isEmbeddingFamily = library.card.capabilities.includes("embedding");
23219
- const sized = runnable.filter((t) => t.sizeBytes);
23220
- const best = budget ? isEmbeddingFamily ? sized.sort((a, b) => (a.sizeBytes ?? 0) - (b.sizeBytes ?? 0))[0] : sized.sort((a, b) => (b.sizeBytes ?? 0) - (a.sizeBytes ?? 0))[0] : void 0;
23221
- const tagPage = pageLiveList(annotated, limit, cursor, (tag) => tag.tag);
23222
- return structuredResult({
23223
- family,
23224
- sourcePage: ref.url,
23225
- library: {
23226
- status: library.status,
23227
- sourceUrl: library.sourceUrl,
23228
- tagsSourceUrl: library.tagsSourceUrl,
23229
- fetchedAt: library.fetchedAt,
23230
- expiresAt: library.expiresAt,
23231
- cached: library.cached,
23232
- ...publicModelGuidance(library, void 0, includeReadme)
23233
- },
23234
- ...tags.length === 0 ? {
23235
- tagsUnavailable: `No pullable tags found for "${family}". Either it is cloud-only (no local download), the family name is wrong, or ollama.com changed its markup. Open the page above to check before concluding the model does not exist.`
23236
- } : {},
23237
- machineMemoryBytes: memoryBytes,
23238
- fitBudgetBytes: budget,
23239
- tags: tagPage.items,
23240
- page: { returned: tagPage.returned, total: tagPage.total, next_cursor: tagPage.next_cursor },
23241
- recommendationUnavailable: budget ? void 0 : "No machine profile (freellama serve unreachable), so memory fit could not be checked and no tag is recommended. Start serve, or read the sizes yourself.",
23242
- recommendation: best ? libraryTrialCandidate(best.tag, isEmbeddingFamily) : null
23243
- });
23244
- }
23245
- const params = new URLSearchParams();
23246
- for (const c of capabilities ?? []) params.append("c", c);
23247
- if (query) params.set("q", query);
23248
- if (order === "newest") params.set("o", "newest");
23249
- const search = await ollamaLibrary.search(params);
23250
- const url = search.url;
23251
- const parsed = search.models.slice(0, limit ?? 10);
23252
- const { installed } = await localState();
23253
- const installedFamilies = new Set([...installed].map((n) => n.split(":")[0]));
23254
- return structuredResult({
23255
- query: url,
23256
- fetchedAt: search.fetchedAt,
23257
- ...parsed.length === 0 ? { searchUnavailable: "No result cards were recognized. The query may have no matches or Ollama's markup may have changed; inspect the source URL." } : {},
23258
- order: order ?? "popular",
23259
- count: parsed.length,
23260
- nextStep: 'Not pullable yet. Call again with view:"library", model:"<name>" to get tags, sizes and memory fit.',
23261
- models: parsed.map((m) => ({ ...m, installed: installedFamilies.has(m.name) }))
23262
- });
23263
- } catch (error2) {
23264
- return errorResult(error2);
23265
- }
23266
23594
  }
23267
- registerContextTools(server);
23268
- server.registerTool(
23269
- "run_task",
23270
- {
23271
- description: "Use when: supplied chat/tools/embeddings. Do not use when: files. Returns: response/receipts.",
23272
- inputSchema: external_exports.object({
23273
- endpoint: endpointParam,
23274
- task: taskParam.removeDefault().optional().describe("caller owns prompts and output format."),
23275
- objective: objectiveParam,
23276
- model: external_exports.string().min(1).optional(),
23277
- sessionId: external_exports.string().uuid().optional(),
23278
- scopeId: external_exports.string().uuid().optional(),
23279
- scopeRevision: external_exports.number().int().nonnegative().optional(),
23280
- contextTokens: external_exports.number().int().positive().optional().describe("Total input + output window (num_ctx)."),
23281
- executionPreference: executionPreferenceParam,
23282
- minPlacementEvidence: minPlacementEvidenceParam,
23283
- requiredCapabilities: requiredCapabilitiesParam,
23284
- prompt: external_exports.string().min(1).optional(),
23285
- systemPrompt: systemPromptParam,
23286
- images: external_exports.array(external_exports.string().min(1)).min(1).optional().describe("base64, no data-URI prefix; prompt mode only; requires an explicit tested vision model"),
23287
- messages: messagesParam.describe(
23288
- "Caller system prompts; no injected task instructions. Overrides prompt; extra fields preserved."
23289
- ),
23290
- input: external_exports.union([external_exports.string().min(1), external_exports.array(external_exports.string().min(1)).min(1)]).optional(),
23291
- tools: localToolsParam,
23292
- keepAlive: external_exports.string().min(1).optional().describe('"0" unloads now, "-1" pins; omitted = adaptive finite TTL'),
23293
- format: external_exports.union([external_exports.literal("json"), external_exports.record(external_exports.unknown())]).optional(),
23294
- think: external_exports.union([external_exports.boolean(), external_exports.enum(["low", "medium", "high"])]).optional(),
23295
- options: external_exports.record(external_exports.unknown()).optional().describe("num_ctx/contextTokens and num_gpu/placement are routing-owned; num_predict caps output."),
23296
- logprobs: external_exports.boolean().optional(),
23297
- topLogprobs: external_exports.number().int().nonnegative().optional(),
23298
- minConfidence: minConfidenceParam,
23299
- priority: external_exports.enum(["interactive", "normal", "background"]).optional(),
23300
- maxWaitSeconds: external_exports.number().int().positive().optional(),
23301
- timeoutSeconds: external_exports.number().int().positive().optional(),
23302
- defer: external_exports.boolean().optional(),
23303
- returnEmbeddings: external_exports.boolean().optional(),
23304
- preview: external_exports.boolean().optional().describe(
23305
- "true = routing fields only; rejects payloads and runtime controls; never executes"
23306
- )
23307
- }).strict(),
23308
- outputSchema: taskResultSchema,
23309
- annotations: { destructiveHint: false }
23310
- },
23311
- async ({
23312
- endpoint,
23313
- task,
23314
- objective,
23315
- model,
23316
- sessionId,
23317
- scopeId,
23318
- scopeRevision,
23319
- contextTokens,
23320
- executionPreference,
23321
- minPlacementEvidence,
23322
- requiredCapabilities,
23323
- prompt,
23324
- systemPrompt,
23325
- images,
23326
- messages,
23327
- input,
23328
- tools,
23329
- keepAlive,
23330
- format,
23331
- think,
23332
- options,
23333
- logprobs,
23334
- topLogprobs,
23335
- minConfidence,
23336
- priority,
23337
- maxWaitSeconds,
23338
- timeoutSeconds,
23339
- defer,
23340
- returnEmbeddings,
23341
- preview
23342
- }) => {
23595
+ var MODEL_EVIDENCE = (() => {
23596
+ const configured = process.env.FREELLAMA_MCP_MODEL_EVIDENCE;
23597
+ const candidates = [
23598
+ configured,
23599
+ path4.join(REPO_ROOT, "packages/mcp/evidence/model-evidence.json")
23600
+ ].filter((x) => Boolean(x));
23601
+ for (const file of candidates) {
23343
23602
  try {
23344
- const canonicalTask = canonicalTaskKind(task ?? "completion");
23345
- if (scopeId === void 0 !== (scopeRevision === void 0)) {
23346
- return errorResult(new Error("scopeId and scopeRevision must be supplied together."));
23347
- }
23348
- if (preview) {
23349
- const executionOnlyFields = [
23350
- ["scopeId", scopeId],
23351
- ["scopeRevision", scopeRevision],
23352
- ["prompt", prompt],
23353
- ["systemPrompt", systemPrompt],
23354
- ["images", images],
23355
- ["messages", messages],
23356
- ["input", input],
23357
- ["tools", tools],
23358
- ["keepAlive", keepAlive],
23359
- ["format", format],
23360
- ["think", think],
23361
- ["options", options],
23362
- ["logprobs", logprobs],
23363
- ["topLogprobs", topLogprobs],
23364
- ["returnEmbeddings", returnEmbeddings],
23365
- ["maxWaitSeconds", maxWaitSeconds],
23366
- ["priority", priority],
23367
- ["timeoutSeconds", timeoutSeconds],
23368
- ["defer", defer]
23369
- ].filter(([, value]) => value !== void 0).map(([name]) => `\`${name}\``);
23370
- if (executionOnlyFields.length > 0) {
23371
- return errorResult(
23372
- new Error(
23373
- `\`preview:true\` accepts routing fields only; remove execution-only fields ${executionOnlyFields.join(", ")}. Use \`requiredCapabilities:["tools"]\` to preview tool capability, then make a separate execution call with the payload.`
23374
- )
23375
- );
23376
- }
23377
- }
23378
- const effectiveRequiredCapabilities = tools === void 0 ? requiredCapabilities : withRequiredCapability(requiredCapabilities, "tools");
23379
- if (topLogprobs !== void 0 && logprobs !== true) {
23380
- return errorResult(new Error("`topLogprobs` requires `logprobs:true`."));
23381
- }
23382
- if (!preview && task === "embedding") {
23383
- if (input === void 0) return errorResult(new Error('task "embedding" requires `input`.'));
23384
- if ([prompt, systemPrompt, images, messages, tools, format, think, logprobs, topLogprobs].some(
23385
- (value) => value !== void 0
23386
- )) {
23387
- return errorResult(
23388
- new Error('task "embedding" accepts `input`, `options`, and routing fields; chat controls are not valid.')
23389
- );
23390
- }
23391
- }
23392
- if (!preview && task !== "embedding") {
23393
- if (input !== void 0 || returnEmbeddings !== void 0) {
23394
- return errorResult(new Error('`input` and `returnEmbeddings` are valid only for task "embedding".'));
23395
- }
23396
- if (prompt === void 0 && systemPrompt === void 0 && (messages === void 0 || messages.length === 0)) {
23397
- return errorResult(new Error(`task "${task}" requires \`prompt\`, \`systemPrompt\`, or \`messages\`.`));
23398
- }
23399
- if (images !== void 0 && (messages !== void 0 || prompt === void 0)) {
23400
- return errorResult(
23401
- new Error("Top-level `images` is valid only with `prompt`; put images inside messages instead.")
23402
- );
23403
- }
23404
- if (images !== void 0 && model === void 0) {
23405
- return errorResult(new Error("Images require an explicit tested vision-capable `model`."));
23406
- }
23407
- }
23408
- if (preview) {
23409
- const result2 = parsedResult(
23410
- await route2(endpoint, canonicalTask, objective, model, sessionId, contextTokens, effectiveRequiredCapabilities, minConfidence, executionPreference, minPlacementEvidence)
23411
- );
23412
- if (isStructuredSuccess(result2)) {
23413
- const refusal = belowConfidence(result2.structuredContent, minConfidence);
23414
- if (refusal) return refusal;
23415
- }
23416
- return result2;
23417
- }
23418
- if (minConfidence && scopeId === void 0) {
23419
- const decision = parsedResult(
23420
- // minConfidence is forwarded so the CORE gate refuses, with its actionable message naming
23421
- // the two commands that raise the grade. The belowConfidence() check below stays only as a
23422
- // fallback for servers older than the core gate.
23423
- await route2(endpoint, canonicalTask, objective, model, sessionId, contextTokens, effectiveRequiredCapabilities, minConfidence, executionPreference, minPlacementEvidence)
23424
- );
23425
- if (!isStructuredSuccess(decision)) return decision;
23426
- const refusal = belowConfidence(decision.structuredContent, minConfidence);
23427
- if (refusal) return refusal;
23428
- }
23429
- const result = parsedResult(
23430
- await runTaskRequest2(endpoint ?? DEFAULT_SERVE_ENDPOINT, {
23431
- task: scopeId !== void 0 && task === void 0 ? void 0 : canonicalTask,
23432
- objective: objective ?? (scopeId === void 0 ? "balanced" : void 0),
23433
- model,
23434
- session_id: sessionId,
23435
- scope_id: scopeId,
23436
- scope_revision: scopeRevision,
23437
- context_tokens: contextTokens,
23438
- required_capabilities: effectiveRequiredCapabilities,
23439
- prompt,
23440
- images,
23441
- messages: taskMessages({ systemPrompt, messages, prompt, images }),
23442
- input,
23443
- tools,
23444
- keep_alive: keepAlive,
23445
- min_confidence: minConfidence,
23446
- priority: priority ?? "normal",
23447
- max_wait_seconds: maxWaitSeconds,
23448
- timeout_seconds: timeoutSeconds,
23449
- defer: defer ?? false,
23450
- execution_preference: executionPreference,
23451
- min_placement_evidence: minPlacementEvidence,
23452
- request_options: {
23453
- format,
23454
- think,
23455
- options,
23456
- logprobs,
23457
- top_logprobs: topLogprobs
23458
- }
23459
- })
23460
- );
23461
- if (isStructuredSuccess(result) && result.structuredContent.deferred === true) return result;
23462
- if (!returnEmbeddings && isStructuredSuccess(result)) {
23463
- const trimmed = summarizeEmbeddings(result.structuredContent);
23464
- if (trimmed) return withTaskTelemetry(structuredResult(trimmed));
23465
- }
23466
- return withTaskTelemetry(result);
23603
+ if (!existsSync4(file)) continue;
23604
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
23605
+ return parsed.models ?? {};
23467
23606
  } catch (error2) {
23468
- return errorResult(error2);
23607
+ process.stderr.write(
23608
+ `freellama-mcp: ignoring unreadable model evidence ${file}: ${error2 instanceof Error ? error2.message : String(error2)}
23609
+ `
23610
+ );
23469
23611
  }
23470
23612
  }
23471
- );
23472
- server.registerTool(
23473
- "task_jobs",
23474
- {
23475
- description: "Use when: task jobs. Do not use when: new work/session kill. Returns: status/result; remove cancels first.",
23476
- inputSchema: external_exports.object({
23477
- endpoint: endpointParam,
23478
- action: external_exports.enum(["list", "get", "cancel", "remove"]),
23479
- jobId: external_exports.string().uuid().optional(),
23480
- returnEmbeddings: external_exports.boolean().optional()
23481
- }).strict(),
23482
- outputSchema: objectResultSchema,
23483
- annotations: { destructiveHint: true }
23484
- },
23485
- async ({ endpoint, action, jobId, returnEmbeddings }) => {
23486
- try {
23487
- if (action === "list" ? jobId !== void 0 : jobId === void 0) {
23488
- return errorResult(new Error("Use jobId only with get/cancel/remove; all require a jobId."));
23489
- }
23490
- if (returnEmbeddings !== void 0 && action !== "get") {
23491
- return errorResult(new Error("returnEmbeddings is valid only with get."));
23492
- }
23493
- const result = parsedResult(action === "list" ? await listTaskJobs2(endpoint) : action === "get" ? await getTaskJob2(endpoint, jobId) : action === "cancel" ? await cancelTaskJob2(endpoint, jobId) : await removeTaskJob2(endpoint, jobId));
23494
- if (!returnEmbeddings && isStructuredSuccess(result)) {
23495
- const job = result.structuredContent.job;
23496
- if (job?.result && typeof job.result === "object") {
23497
- const trimmed = summarizeEmbeddings(job.result);
23498
- if (trimmed) return structuredResult({ ...result.structuredContent, job: { ...job, result: trimmed } });
23499
- }
23500
- }
23501
- return result;
23502
- } catch (error2) {
23503
- return errorResult(error2);
23504
- }
23613
+ return {};
23614
+ })();
23615
+ var STRONG_MODEL_HINT = "Re-run with a model graded strong in packages/mcp/evidence/model-evidence.json";
23616
+ function assessDelegatedAnswer(question, evidenceCount, model) {
23617
+ const grounded = evidenceCount > 0;
23618
+ const evidence = MODEL_EVIDENCE[model];
23619
+ if (evidence?.grade === "unusable") {
23620
+ return {
23621
+ recommendation: "escalate",
23622
+ grounded,
23623
+ why: `${model} is not viable for research on this machine (${evidence.note}). It answers fast and confidently while being wrong \u2014 a fast wrong answer is not a speed win. ${STRONG_MODEL_HINT}, or answer it yourself.`,
23624
+ measuredBaseRate: `${model}: ${evidence.note}`
23625
+ };
23505
23626
  }
23506
- );
23507
- server.registerTool(
23508
- "run_task_batch",
23509
- {
23510
- description: "Use when: independent:true work. Do not use when: dependencies. Returns: ordered receipts; maxParallelism caps dispatch.",
23511
- inputSchema: external_exports.object({
23512
- tasks: external_exports.array(batchItemParam).min(1).max(64),
23513
- maxParallelism: external_exports.number().int().positive().max(64).optional(),
23514
- endpoint: endpointParam
23515
- }).strict(),
23516
- outputSchema: batchResultSchema,
23517
- annotations: { destructiveHint: false }
23518
- },
23519
- async ({ tasks, maxParallelism, endpoint }) => {
23520
- try {
23521
- for (const item of tasks) {
23522
- const task = item.task;
23523
- if (task.scopeId === void 0 !== (task.scopeRevision === void 0)) {
23524
- return errorResult(new Error(`batch item ${item.id}: scopeId and scopeRevision must be supplied together.`));
23525
- }
23526
- if (task.task === "embedding") {
23527
- if (task.input === void 0 || task.prompt !== void 0 || task.systemPrompt !== void 0 || task.messages !== void 0 || task.tools !== void 0) {
23528
- return errorResult(new Error(`batch item ${item.id}: embedding requires input and accepts no chat payload.`));
23529
- }
23530
- } else if (task.input !== void 0 || task.prompt === void 0 && task.systemPrompt === void 0 && task.messages === void 0) {
23531
- return errorResult(new Error(`batch item ${item.id}: chat work requires prompt, systemPrompt, or messages and accepts no input.`));
23532
- }
23533
- if (task.images !== void 0 && (task.messages !== void 0 || task.prompt === void 0)) {
23534
- return errorResult(new Error(`batch item ${item.id}: top-level images requires prompt without messages; put images inside messages instead.`));
23535
- }
23536
- if (task.images !== void 0 && task.model === void 0) {
23537
- return errorResult(new Error(`batch item ${item.id}: images require an explicit tested vision model.`));
23538
- }
23539
- }
23540
- return withBatchTelemetry(parsedResult(await runTaskBatchRequest2(endpoint ?? DEFAULT_SERVE_ENDPOINT, {
23541
- tasks: tasks.map((item) => {
23542
- const task = item.task;
23543
- return {
23544
- id: item.id,
23545
- independent: item.independent,
23546
- task: {
23547
- task: task.task === "code_review" ? "coding" : task.task ?? (task.scopeId === void 0 ? "completion" : void 0),
23548
- objective: task.objective ?? (task.scopeId === void 0 ? "balanced" : void 0),
23549
- model: task.model,
23550
- session_id: task.sessionId,
23551
- scope_id: task.scopeId,
23552
- scope_revision: task.scopeRevision,
23553
- context_tokens: task.contextTokens,
23554
- execution_preference: task.executionPreference,
23555
- min_placement_evidence: task.minPlacementEvidence,
23556
- required_capabilities: task.requiredCapabilities,
23557
- priority: task.priority ?? "normal",
23558
- max_wait_seconds: task.maxWaitSeconds,
23559
- timeout_seconds: task.timeoutSeconds,
23560
- prompt: task.prompt,
23561
- images: task.images,
23562
- messages: taskMessages(task),
23563
- input: task.input,
23564
- tools: task.tools,
23565
- keep_alive: task.keepAlive,
23566
- min_confidence: task.minConfidence,
23567
- request_options: {
23568
- format: task.format,
23569
- think: task.think,
23570
- options: task.options,
23571
- logprobs: task.logprobs,
23572
- top_logprobs: task.topLogprobs
23573
- }
23574
- }
23575
- };
23576
- }),
23577
- max_parallelism: maxParallelism
23578
- })));
23579
- } catch (error2) {
23580
- return errorResult(error2);
23581
- }
23627
+ if (!evidence) {
23628
+ return {
23629
+ recommendation: "verify",
23630
+ grounded,
23631
+ why: `${model} has no measured accuracy in this repo's benchmarks, so no base rate applies to this answer. Treat it as unverified until it has been evaluated \u2014 accuracy fell off a cliff below ~12B in the models that were measured.`,
23632
+ measuredBaseRate: "no measured base rate for this model"
23633
+ };
23582
23634
  }
23583
- );
23584
- server.registerTool(
23585
- "ollama_manage",
23586
- {
23587
- description: "Use when: approved pull/stop. Do not use when: delete/discovery. Returns: receipt; timeoutSeconds is pull-only.",
23588
- inputSchema: external_exports.object({
23589
- action: external_exports.enum(["pull", "stop"]),
23590
- model: external_exports.string().min(1),
23591
- ollamaEndpoint: ollamaEndpointParam,
23592
- timeoutSeconds: external_exports.number().int().positive().optional()
23593
- }).strict(),
23594
- outputSchema: manageResultSchema,
23595
- annotations: { destructiveHint: false }
23596
- },
23597
- async ({ action, model, ollamaEndpoint, timeoutSeconds }, extra) => {
23598
- try {
23599
- if (action === "stop" && timeoutSeconds !== void 0) {
23600
- return errorResult(new Error('`timeoutSeconds` is valid only for action "pull".'));
23601
- }
23602
- const data = action === "pull" ? await ollamaPull(ollamaEndpoint, model, timeoutSeconds, {
23603
- signal: extra.signal,
23604
- onProgress: pullProgressReporter(extra)
23605
- }) : await ollamaFetch(ollamaEndpoint, "/api/generate", {
23606
- method: "POST",
23607
- body: { model, keep_alive: 0 },
23608
- signal: extra.signal
23609
- });
23610
- const payload = data;
23611
- if (typeof payload.error === "string" && payload.error) {
23612
- return errorResult(new Error(`${action} ${model} failed: ${payload.error}`));
23613
- }
23614
- return structuredResult(payload);
23615
- } catch (error2) {
23616
- return errorResult(error2);
23617
- }
23635
+ if (evidence.grade === "weak" && grounded) {
23636
+ return {
23637
+ recommendation: "verify",
23638
+ grounded,
23639
+ why: `${model} holds up on simple single-file lookups but not beyond (${evidence.note}). Check the evidence trail, or ${STRONG_MODEL_HINT.toLowerCase()} if the answer matters.`,
23640
+ measuredBaseRate: `${model}: ${evidence.note}`
23641
+ };
23618
23642
  }
23619
- );
23620
- server.registerTool(
23621
- "ollama_delete",
23622
- {
23623
- description: "DESTRUCTIVE AND IRREVERSIBLE. Use when: human names exact tag. Do not use when: inferred cleanup. Returns: deleted tag.",
23624
- inputSchema: external_exports.object({
23625
- model: external_exports.string().min(1),
23626
- ollamaEndpoint: ollamaEndpointParam
23627
- }).strict(),
23628
- outputSchema: deleteResultSchema,
23629
- annotations: { destructiveHint: true }
23630
- },
23631
- async ({ model, ollamaEndpoint }) => {
23643
+ if (!grounded) {
23644
+ return {
23645
+ recommendation: "escalate",
23646
+ grounded: false,
23647
+ why: "The model answered without reading a single file, so this is parametric recall, not research \u2014 the failure mode where `run_task` was verified inventing wrong facts about this project's own architecture. Re-ask with a narrower question, or answer it yourself.",
23648
+ measuredBaseRate: "ungrounded answers have no measured accuracy \u2014 they were never the tested path"
23649
+ };
23650
+ }
23651
+ const judgmentSignals = /\b(should (we|i|you)|better|best|worth|review|assess|evaluate|improve|opinion|recommend|why is|is it (good|safe|correct)|design|refactor)\b/i;
23652
+ if (judgmentSignals.test(question)) {
23653
+ return {
23654
+ recommendation: "verify",
23655
+ grounded: true,
23656
+ why: "The question reads as a judgment call (keyword heuristic). Measured accuracy on judgment calls was well below grounded lookups, and the tone is identical either way. Check the evidence trail against the claim before acting on it.",
23657
+ measuredBaseRate: `${model}: ${evidence.note}; measured on lookups, not judgment calls`
23658
+ };
23659
+ }
23660
+ if (evidenceCount > 5) {
23661
+ return {
23662
+ recommendation: "verify",
23663
+ grounded: true,
23664
+ why: `${evidenceCount} tool calls is outside the 1-5 file envelope this tool was measured on. Wide searches are where it drifts; spot-check the evidence trail.`,
23665
+ measuredBaseRate: `${model}: ${evidence.note}; measured within a 1-5 file scope`
23666
+ };
23667
+ }
23668
+ return {
23669
+ recommendation: "accept",
23670
+ grounded: true,
23671
+ why: `Grounded in ${evidenceCount} tool call(s) within the measured 1-5 file envelope, the question is lookup-shaped, and ${model} is measured strong for this (${evidence.note}).`,
23672
+ measuredBaseRate: `${model}: ${evidence.note}`
23673
+ };
23674
+ }
23675
+ function extractExistingWorkspacePath(command, workspace) {
23676
+ let root;
23677
+ try {
23678
+ root = realpathSync(workspace);
23679
+ } catch {
23680
+ return null;
23681
+ }
23682
+ const tokens = command.match(/(?:[^\s"'\\]+|"(?:\\.|[^"])*"|'[^']*')+/g) ?? [];
23683
+ const patternFlags = /* @__PURE__ */ new Set(["-name", "-iname", "-path", "-ipath", "-regex", "-iregex", "-wholename"]);
23684
+ for (let index = 0; index < tokens.length; index += 1) {
23685
+ const raw = tokens[index] ?? "";
23686
+ if (index > 0 && patternFlags.has(tokens[index - 1] ?? "")) continue;
23687
+ const token = raw.replace(/^["']|["']$/g, "").replace(/[|;&)]+$/g, "").replace(/:\d+(?::\d+)?$/, "");
23688
+ if (!token || token.startsWith("-") || /[*?$`]/.test(token)) continue;
23689
+ const candidate = path4.isAbsolute(token) ? token : path4.resolve(root, token);
23690
+ if (!existsSync4(candidate)) continue;
23632
23691
  try {
23633
- await ollamaFetch(ollamaEndpoint, "/api/delete", {
23634
- method: "DELETE",
23635
- body: { model }
23636
- });
23637
- return structuredResult({ deleted: model });
23638
- } catch (error2) {
23639
- return errorResult(error2);
23692
+ const resolved = realpathSync(candidate);
23693
+ const relative = path4.relative(root, resolved);
23694
+ const withinRoot = relative && relative !== ".." && !relative.startsWith(`..${path4.sep}`);
23695
+ if (withinRoot && statSync(resolved).isFile()) return relative;
23696
+ } catch {
23640
23697
  }
23641
23698
  }
23642
- );
23643
- async function routedResearchModel(endpoint, executionPreference, minPlacementEvidence) {
23699
+ return null;
23700
+ }
23701
+ var adapterCallSchema = external_exports.object({
23702
+ raw_name: external_exports.string().optional(),
23703
+ status: external_exports.string().optional(),
23704
+ arguments: external_exports.object({
23705
+ tool: external_exports.string().optional(),
23706
+ command: external_exports.string().optional(),
23707
+ queries: external_exports.object({ path: external_exports.string().optional() }).passthrough().optional()
23708
+ }).passthrough().optional()
23709
+ }).passthrough();
23710
+ var adapterResultSchema = external_exports.object({
23711
+ final_answer: external_exports.string(),
23712
+ tool_calls: external_exports.array(adapterCallSchema).default([]),
23713
+ usage: external_exports.object({
23714
+ input_tokens: external_exports.number().nullable().optional(),
23715
+ output_tokens: external_exports.number().nullable().optional()
23716
+ }).passthrough().default({}),
23717
+ model_metadata: external_exports.object({
23718
+ context_compactions: external_exports.number().int().nonnegative().optional(),
23719
+ runtime_config: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
23720
+ context_management: external_exports.object({
23721
+ token_counting: external_exports.enum(["configured_estimate", "model_calibrated_estimate"]),
23722
+ estimate_scale: external_exports.number().positive(),
23723
+ calibration_samples: external_exports.number().int().nonnegative(),
23724
+ pinned_overflow: external_exports.enum(["error", "clip"]),
23725
+ compactions: external_exports.number().int().nonnegative()
23726
+ }).passthrough().optional()
23727
+ }).passthrough().optional()
23728
+ });
23729
+ function parseAdapterResult(raw) {
23730
+ return adapterResultSchema.parse(JSON.parse(raw));
23731
+ }
23732
+
23733
+ // src/tools/delegate-research.ts
23734
+ var researchResultSchema = external_exports.object({
23735
+ answer: external_exports.string(),
23736
+ summary: external_exports.string(),
23737
+ citations: external_exports.unknown().optional(),
23738
+ verification: external_exports.unknown(),
23739
+ usage: external_exports.unknown().optional(),
23740
+ telemetry: external_exports.unknown().optional()
23741
+ }).passthrough();
23742
+ async function routedResearchModel(endpoint) {
23644
23743
  const decision = JSON.parse(
23645
- await route2(endpoint, "coding", "balanced", void 0, void 0, void 0, [], void 0, executionPreference, minPlacementEvidence)
23744
+ await route2(endpoint, "coding", "balanced", void 0, void 0, void 0, [], void 0, void 0, void 0)
23646
23745
  );
23647
23746
  if (typeof decision.selected_model !== "string" || !decision.selected_model) {
23648
23747
  throw new Error(
@@ -23651,196 +23750,202 @@ async function routedResearchModel(endpoint, executionPreference, minPlacementEv
23651
23750
  }
23652
23751
  return decision.selected_model;
23653
23752
  }
23654
- server.registerTool(
23655
- "delegate_research",
23656
- {
23657
- description: "Use when: workspace lookup. Do not use when: mutation/external facts. Returns: citations/answer; discard verification.recommendation=escalate, otherwise verify citations.",
23658
- inputSchema: external_exports.object({
23659
- question: external_exports.string().min(1),
23660
- workspacePath: external_exports.string().min(1),
23661
- adapter: external_exports.enum(["bash", "octocode"]).optional(),
23662
- model: external_exports.string().min(1).optional(),
23663
- endpoint: endpointParam,
23664
- executionPreference: executionPreferenceParam,
23665
- minPlacementEvidence: minPlacementEvidenceParam,
23666
- legacyText: external_exports.boolean().optional(),
23667
- agent: external_exports.object({
23668
- maxTurns: external_exports.number().int().positive().optional(),
23669
- contextTokens: external_exports.number().int().positive().optional(),
23670
- outputTokens: external_exports.number().int().positive().optional(),
23671
- temperature: external_exports.number().nonnegative().optional(),
23672
- seed: external_exports.number().int().nonnegative().optional(),
23673
- think: external_exports.boolean().optional(),
23674
- keepAlive: external_exports.string().min(1).optional(),
23675
- requestTimeoutSeconds: external_exports.number().positive().optional(),
23676
- toolTimeoutSeconds: external_exports.number().positive().optional()
23677
- }).strict().optional()
23678
- }).strict(),
23679
- outputSchema: researchResultSchema,
23680
- annotations: { destructiveHint: false }
23681
- },
23682
- async ({ question, workspacePath, adapter, model, endpoint, executionPreference, minPlacementEvidence, legacyText, agent }, extra) => {
23683
- const chosenAdapter = adapter ?? DEFAULT_RESEARCH_ADAPTER;
23684
- let resolvedWorkspace;
23685
- let chosenModel;
23686
- try {
23687
- resolvedWorkspace = await assertAllowedWorkspace(workspacePath);
23688
- chosenModel = model ?? DEFAULT_DELEGATE_MODEL ?? await routedResearchModel(endpoint, executionPreference, minPlacementEvidence);
23689
- } catch (error2) {
23690
- return researchErrorResult(error2);
23691
- }
23692
- const known = MODEL_EVIDENCE[chosenModel];
23693
- if (known?.grade === "unusable") {
23694
- return structuredResult({
23695
- adapter: chosenAdapter,
23696
- verification: assessDelegatedAnswer(question, 0, chosenModel),
23697
- answer: "",
23698
- toolCallCount: 0,
23699
- usage: { inputTokens: null, outputTokens: null },
23700
- telemetry: costTelemetry({ inputTokens: null, outputTokens: null }, EXTERNAL_COST),
23701
- evidence: [],
23702
- summary: `Refused before running: ${chosenModel} is measured unusable for research here (${known.note}). Re-run with a ~27B model (see README), or answer it yourself.`
23703
- });
23704
- }
23705
- const dir = await mkdtemp(path5.join(tmpdir(), "freellama-delegate-"));
23706
- const promptFile = path5.join(dir, "prompt.md");
23707
- const resultFile = path5.join(dir, "result.json");
23708
- try {
23709
- await writeFile(promptFile, `${question}
23710
- `, "utf8");
23711
- const adapter2 = RESEARCH_ADAPTERS[chosenAdapter];
23712
- if (!existsSync5(adapter2)) {
23713
- return researchErrorResult(
23714
- new Error(
23715
- `research adapter not found at ${adapter2}. In a published install it should be bundled under <package>/adapters; in-repo it comes from benchmark/local/scripts. Reinstall, or run \`npm run build\` from packages/mcp/ to re-copy it.`
23716
- )
23717
- );
23718
- }
23719
- const running = execFileAsync("python3", [adapter2], {
23720
- signal: extra.signal,
23721
- env: {
23722
- ...delegateEnvironment(),
23723
- FREELLAMA_TARGET_MODEL: chosenModel,
23724
- FREELLAMA_AGENT_MANAGED_ENDPOINT: endpoint ?? DEFAULT_SERVE_ENDPOINT,
23725
- FREELLAMA_AGENT_EXECUTION_PREFERENCE: executionPreference ?? "auto",
23726
- FREELLAMA_AGENT_MIN_PLACEMENT_EVIDENCE: minPlacementEvidence ?? "configured",
23727
- FREELLAMA_AGENT_TOKEN_CALIBRATION_DIR: DEFAULT_TOKEN_CALIBRATION_DIR,
23728
- FREELLAMA_BENCH_WORKSPACE: resolvedWorkspace,
23729
- FREELLAMA_BENCH_PROMPT: promptFile,
23730
- FREELLAMA_AGENT_RESULT: resultFile,
23731
- FREELLAMA_AGENT_MAX_TURNS: String(agent?.maxTurns ?? DEFAULT_DELEGATE_MAX_TURNS),
23732
- ...agent?.contextTokens !== void 0 ? { FREELLAMA_AGENT_NUM_CTX: String(agent.contextTokens) } : {},
23733
- ...agent?.outputTokens !== void 0 ? { FREELLAMA_AGENT_NUM_PREDICT: String(agent.outputTokens) } : {},
23734
- ...agent?.temperature !== void 0 ? { FREELLAMA_AGENT_TEMPERATURE: String(agent.temperature) } : {},
23735
- ...agent?.seed !== void 0 ? { FREELLAMA_AGENT_SEED: String(agent.seed) } : {},
23736
- ...agent?.think !== void 0 ? { FREELLAMA_AGENT_THINK: String(agent.think) } : {},
23737
- ...agent?.keepAlive !== void 0 ? { FREELLAMA_AGENT_KEEP_ALIVE: agent.keepAlive } : {},
23738
- ...agent?.requestTimeoutSeconds !== void 0 ? { FREELLAMA_AGENT_REQUEST_TIMEOUT_SECONDS: String(agent.requestTimeoutSeconds) } : {},
23739
- ...agent?.toolTimeoutSeconds !== void 0 ? { FREELLAMA_AGENT_TOOL_TIMEOUT_SECONDS: String(agent.toolTimeoutSeconds) } : {}
23740
- },
23741
- timeout: DEFAULT_DELEGATE_TIMEOUT_SECONDS * 1e3,
23742
- // The answer is read from `resultFile`, never from stdout — but execFile's default 1 MB
23743
- // maxBuffer still applies to the agent's own progress logging, and overflowing it kills
23744
- // the subprocess and surfaces as a research failure with no explanation. Give the logs
23745
- // room; nothing here is proportional to the size of the answer.
23746
- maxBuffer: 32 * 1024 * 1024,
23747
- // On timeout, don't negotiate: SIGTERM can be swallowed by a python process blocked in a
23748
- // long model call, which would leave the child (and its VRAM) alive past the deadline the
23749
- // caller was promised.
23750
- killSignal: "SIGKILL"
23751
- });
23752
- liveDelegates.add(running.child);
23753
- let adapterError = null;
23753
+ function registerDelegateResearchTool(server2) {
23754
+ server2.registerTool(
23755
+ "delegate_research",
23756
+ {
23757
+ description: "Use when: a bounded read-only question about files under workspacePath (where is X, what calls Y). Do not use when: editing, external facts, or final judgment. Returns: citations/answer; discard verification.recommendation=escalate, otherwise verify citations.",
23758
+ inputSchema: external_exports.object({
23759
+ question: external_exports.string().min(1),
23760
+ workspacePath: external_exports.string().min(1),
23761
+ adapter: external_exports.enum(["bash", "octocode"]).optional(),
23762
+ model: external_exports.string().min(1).optional(),
23763
+ endpoint: endpointParam,
23764
+ agent: external_exports.object({
23765
+ maxTurns: external_exports.number().int().positive().optional(),
23766
+ contextTokens: external_exports.number().int().positive().optional(),
23767
+ outputTokens: external_exports.number().int().positive().optional()
23768
+ }).strict().optional()
23769
+ }).strict(),
23770
+ outputSchema: researchResultSchema,
23771
+ annotations: { destructiveHint: false }
23772
+ },
23773
+ async ({ question, workspacePath, adapter, model, endpoint, agent }, extra) => {
23774
+ const chosenAdapter = adapter ?? DEFAULT_RESEARCH_ADAPTER;
23775
+ let resolvedWorkspace;
23776
+ let chosenModel;
23754
23777
  try {
23755
- await running;
23778
+ resolvedWorkspace = await assertAllowedWorkspace(workspacePath);
23779
+ chosenModel = model ?? DEFAULT_DELEGATE_MODEL ?? await routedResearchModel(endpoint);
23756
23780
  } catch (error2) {
23757
- adapterError = error2;
23758
- } finally {
23759
- liveDelegates.delete(running.child);
23781
+ return researchErrorResult(error2);
23760
23782
  }
23761
- let result;
23762
- try {
23763
- result = parseAdapterResult(await readFile(resultFile, "utf8"));
23764
- } catch {
23765
- return researchErrorResult(
23766
- adapterError ?? new Error(
23767
- `research adapter exited without writing a readable result file \u2014 it was killed before it could report. Check that the model is loadable and that ${DEFAULT_DELEGATE_TIMEOUT_SECONDS}s is enough for this question.`
23768
- )
23769
- );
23783
+ const known = MODEL_EVIDENCE[chosenModel];
23784
+ if (known?.grade === "unusable") {
23785
+ return structuredResult({
23786
+ adapter: chosenAdapter,
23787
+ verification: assessDelegatedAnswer(question, 0, chosenModel),
23788
+ answer: "",
23789
+ toolCallCount: 0,
23790
+ usage: { inputTokens: null, outputTokens: null },
23791
+ telemetry: costTelemetry({ inputTokens: null, outputTokens: null }, EXTERNAL_COST),
23792
+ evidence: [],
23793
+ summary: `Refused before running: ${chosenModel} is measured unusable for research here (${known.note}). ${STRONG_MODEL_HINT}, or answer it yourself.`
23794
+ });
23770
23795
  }
23771
- const evidence = result.tool_calls.map((call, index) => {
23772
- const target = call.arguments?.queries?.path;
23773
- const rawDetail = call.arguments?.command ?? null;
23774
- const detail = rawDetail ? clipText(rawDetail, 400) : null;
23775
- return {
23776
- step: index + 1,
23777
- tool: call.arguments?.tool ?? call.raw_name ?? "?",
23778
- // The adapters record "ok" | "error" | "repeat" per call. Carrying it through is what
23779
- // lets a reader tell a run that read three files from one that failed three commands —
23780
- // indistinguishable in the trail before, and graded identically.
23781
- status: call.status ?? "ok",
23782
- path: target ? path5.relative(resolvedWorkspace, target) : detail ? extractExistingWorkspacePath(detail, resolvedWorkspace) : null,
23783
- detail,
23784
- detail_truncated: rawDetail !== detail
23785
- };
23786
- });
23787
- const succeeded = evidence.filter((step) => step.status === "ok");
23788
- const failed = evidence.length - succeeded.length;
23789
- const evidenceText = evidence.map(
23790
- (step) => ` ${step.step}. ${step.tool}${step.status === "ok" ? "" : ` [${step.status}]`}${step.path ? ` -> ${step.path}` : ""}${step.detail ? `: ${clipText(step.detail, 400)}` : ""}`
23791
- ).join("\n");
23792
- const citations = succeeded.map((step) => ({ step: step.step, tool: step.tool, path: step.path }));
23793
- if (adapterError) {
23794
- const diagnostic = `research adapter failed: ${result.final_answer}` + (evidenceText ? `
23796
+ const dir = await mkdtemp(path5.join(tmpdir(), "freellama-delegate-"));
23797
+ const promptFile = path5.join(dir, "prompt.md");
23798
+ const resultFile = path5.join(dir, "result.json");
23799
+ try {
23800
+ await writeFile(promptFile, `${question}
23801
+ `, "utf8");
23802
+ const adapter2 = RESEARCH_ADAPTERS[chosenAdapter];
23803
+ if (!existsSync5(adapter2)) {
23804
+ return researchErrorResult(
23805
+ new Error(
23806
+ `research adapter not found at ${adapter2}. It is compiled to <package>/dist/research by the package build. Reinstall, or run \`yarn build\` from packages/mcp/ to rebuild it.`
23807
+ )
23808
+ );
23809
+ }
23810
+ const running = runDelegate(process.execPath, [adapter2], {
23811
+ signal: extra.signal,
23812
+ env: {
23813
+ ...delegateEnvironment(),
23814
+ FREELLAMA_TARGET_MODEL: chosenModel,
23815
+ FREELLAMA_AGENT_MANAGED_ENDPOINT: endpoint ?? DEFAULT_SERVE_ENDPOINT,
23816
+ FREELLAMA_AGENT_TOKEN_CALIBRATION_DIR: DEFAULT_TOKEN_CALIBRATION_DIR,
23817
+ FREELLAMA_BENCH_WORKSPACE: resolvedWorkspace,
23818
+ FREELLAMA_BENCH_PROMPT: promptFile,
23819
+ FREELLAMA_AGENT_RESULT: resultFile,
23820
+ FREELLAMA_AGENT_MAX_TURNS: String(agent?.maxTurns ?? DEFAULT_DELEGATE_MAX_TURNS),
23821
+ ...agent?.contextTokens !== void 0 ? { FREELLAMA_AGENT_NUM_CTX: String(agent.contextTokens) } : {},
23822
+ ...agent?.outputTokens !== void 0 ? { FREELLAMA_AGENT_NUM_PREDICT: String(agent.outputTokens) } : {}
23823
+ },
23824
+ timeoutMs: DEFAULT_DELEGATE_TIMEOUT_SECONDS * 1e3,
23825
+ // The answer is read from `resultFile`, never from stdout — but execFile's usual 1 MB
23826
+ // maxBuffer still applies to the agent's own progress logging, and overflowing it kills
23827
+ // the subprocess and surfaces as a research failure with no explanation. Give the logs
23828
+ // room; nothing here is proportional to the size of the answer.
23829
+ maxBuffer: 32 * 1024 * 1024
23830
+ // On timeout, abort, or overflow runDelegate SIGKILLs the whole process group: SIGTERM can
23831
+ // be swallowed by an adapter blocked in a long model call, and killing the adapter alone
23832
+ // would orphan `npx octocode` and sandbox shells.
23833
+ });
23834
+ liveDelegates.add(running.child);
23835
+ let adapterError = null;
23836
+ try {
23837
+ await running.done;
23838
+ } catch (error2) {
23839
+ adapterError = error2;
23840
+ } finally {
23841
+ liveDelegates.delete(running.child);
23842
+ killDelegateTree(running.child);
23843
+ }
23844
+ let result;
23845
+ try {
23846
+ result = parseAdapterResult(await readFile2(resultFile, "utf8"));
23847
+ } catch {
23848
+ return researchErrorResult(
23849
+ adapterError ?? new Error(
23850
+ `research adapter exited without writing a readable result file \u2014 it was killed before it could report. Check that the model is loadable and that ${DEFAULT_DELEGATE_TIMEOUT_SECONDS}s is enough for this question.`
23851
+ )
23852
+ );
23853
+ }
23854
+ const evidence = result.tool_calls.map((call, index) => {
23855
+ const target = call.arguments?.queries?.path;
23856
+ const rawDetail = call.arguments?.command ?? null;
23857
+ const detail = rawDetail ? clipText(rawDetail, 400) : null;
23858
+ return {
23859
+ step: index + 1,
23860
+ tool: call.arguments?.tool ?? call.raw_name ?? "?",
23861
+ // The adapters record "ok" | "error" | "repeat" per call. Carrying it through is what
23862
+ // lets a reader tell a run that read three files from one that failed three commands —
23863
+ // indistinguishable in the trail before, and graded identically.
23864
+ status: call.status ?? "ok",
23865
+ path: target ? path5.relative(resolvedWorkspace, target) : detail ? extractExistingWorkspacePath(detail, resolvedWorkspace) : null,
23866
+ detail,
23867
+ detail_truncated: rawDetail !== detail
23868
+ };
23869
+ });
23870
+ const succeeded = evidence.filter((step) => step.status === "ok");
23871
+ const failed = evidence.length - succeeded.length;
23872
+ const evidenceText = evidence.map(
23873
+ (step) => ` ${step.step}. ${step.tool}${step.status === "ok" ? "" : ` [${step.status}]`}${step.path ? ` -> ${step.path}` : ""}${step.detail ? `: ${step.detail}` : ""}`
23874
+ ).join("\n");
23875
+ const citations = succeeded.map((step) => ({ step: step.step, tool: step.tool, path: step.path }));
23876
+ if (adapterError) {
23877
+ const diagnostic = `research adapter failed: ${result.final_answer}` + (evidenceText ? `
23795
23878
  Evidence collected before the failure:
23796
23879
  ${evidenceText}` : "");
23797
- const refusal = external_exports.object({ receipt: external_exports.object({ error: external_exports.string() }).passthrough() }).safeParse(result.model_metadata?.transport_error);
23798
- if (refusal.success) {
23799
- return researchErrorResult(new Error(JSON.stringify(refusal.data.receipt)), diagnostic);
23800
- }
23801
- return researchErrorResult(new Error(diagnostic));
23802
- }
23803
- const verification = assessDelegatedAnswer(question, succeeded.length, chosenModel);
23804
- const summary = `Delegated answer ready: ${result.tool_calls.length} tool call(s)` + (failed > 0 ? `, ${failed} of which did not succeed` : "") + `; ${result.usage.input_tokens ?? "?"} input / ${result.usage.output_tokens ?? "?"} output local tokens; verification=${verification.recommendation}. Read structuredContent.answer and citations.`;
23805
- const payload = {
23806
- adapter: chosenAdapter,
23807
- verification,
23808
- answer: result.final_answer,
23809
- // recommendation + why live under `verification`; `citations` completes the triple so a
23810
- // caller never has to read `summary` to act on the result.
23811
- citations,
23812
- toolCallCount: result.tool_calls.length,
23813
- successfulToolCallCount: succeeded.length,
23814
- usage: {
23815
- inputTokens: result.usage.input_tokens,
23816
- outputTokens: result.usage.output_tokens
23817
- },
23818
- telemetry: costTelemetry({
23819
- inputTokens: result.usage.input_tokens ?? null,
23820
- outputTokens: result.usage.output_tokens ?? null
23821
- }, EXTERNAL_COST),
23822
- contextManagement: result.model_metadata?.context_management ?? null,
23823
- execution: {
23824
- preference: executionPreference ?? "auto",
23825
- minPlacementEvidence: minPlacementEvidence ?? "configured",
23826
- receipts: result.model_metadata?.execution_receipts ?? []
23827
- },
23828
- evidence,
23829
- summary
23830
- };
23831
- const cited = [...new Set(citations.map((citation) => citation.path).filter(Boolean))];
23832
- const answerText = `${clipText(result.final_answer, ANSWER_TEXT_MAX_CHARS)}
23880
+ const refusal = external_exports.object({ receipt: external_exports.object({ error: external_exports.string() }).passthrough() }).safeParse(result.model_metadata?.transport_error);
23881
+ if (refusal.success) {
23882
+ return researchErrorResult(new Error(JSON.stringify(refusal.data.receipt)), diagnostic);
23883
+ }
23884
+ return researchErrorResult(new Error(diagnostic));
23885
+ }
23886
+ const verification = assessDelegatedAnswer(question, succeeded.length, chosenModel);
23887
+ const summary = `Delegated answer ready: ${result.tool_calls.length} tool call(s)` + (failed > 0 ? `, ${failed} of which did not succeed` : "") + `; ${result.usage.input_tokens ?? "?"} input / ${result.usage.output_tokens ?? "?"} output local tokens; verification=${verification.recommendation}. Read structuredContent.answer and citations.`;
23888
+ const payload = {
23889
+ adapter: chosenAdapter,
23890
+ verification,
23891
+ answer: result.final_answer,
23892
+ // recommendation + why live under `verification`; `citations` completes the triple so a
23893
+ // caller never has to read `summary` to act on the result.
23894
+ citations,
23895
+ toolCallCount: result.tool_calls.length,
23896
+ successfulToolCallCount: succeeded.length,
23897
+ usage: {
23898
+ inputTokens: result.usage.input_tokens,
23899
+ outputTokens: result.usage.output_tokens
23900
+ },
23901
+ telemetry: costTelemetry({
23902
+ inputTokens: result.usage.input_tokens ?? null,
23903
+ outputTokens: result.usage.output_tokens ?? null
23904
+ }, EXTERNAL_COST),
23905
+ contextManagement: result.model_metadata?.context_management ?? null,
23906
+ execution: { receipts: result.model_metadata?.execution_receipts ?? [] },
23907
+ evidence,
23908
+ summary
23909
+ };
23910
+ const cited = [...new Set(citations.map((citation) => citation.path).filter(Boolean))];
23911
+ const answerText = `${clipText(result.final_answer, ANSWER_TEXT_MAX_CHARS)}
23833
23912
 
23834
23913
  verification: ${verification.recommendation}` + (cited.length ? `
23835
23914
  cited: ${cited.slice(0, 20).join(", ")}` : "") + `
23836
23915
  (${chosenModel}, ${result.tool_calls.length} tool call(s))`;
23837
- return structuredResult(payload, legacyText === true ? { legacyJson: true } : { text: answerText });
23838
- } catch (error2) {
23839
- return researchErrorResult(error2);
23840
- } finally {
23841
- await rm(dir, { recursive: true, force: true });
23916
+ return structuredResult(payload, { text: answerText });
23917
+ } catch (error2) {
23918
+ return researchErrorResult(error2);
23919
+ } finally {
23920
+ await rm(dir, { recursive: true, force: true });
23921
+ }
23842
23922
  }
23843
- }
23923
+ );
23924
+ }
23925
+
23926
+ // src/index.ts
23927
+ installProcessLifecycle();
23928
+ var INSTRUCTIONS = `models{view:"installed"}; models{view:"resident"}.
23929
+ run_task preview never executes; code_review aliases coding. requiredCapabilities:["tools"] previews a tool route; omit preview and supply the payload. Caller runs tool calls.
23930
+ quality objective needs a task policy or explicit model; medium confidence needs policy+benchmark; observed placement needs /api/ps proof.
23931
+ Scoped run_task needs scopeId+scopeRevision. defer:true: job.id\u2192task_jobs.jobId unchanged. Restart loses scopes/sessions/jobs.
23932
+ Check isError first; prefer structuredContent, else content[].text. page.next_cursor\u2192cursor unchanged.
23933
+ caller owns task decomposition/prompts/format/verification; findings are candidates, not accepted defects. operator owns endpoints, exact --cpu-model assignments/lifecycle. Ollama plus the OS/driver run physical CPU/GPU.
23934
+ ask approval for one exact tag and reported size before ollama_manage pull; search or recommendation is never download permission. Stop/delete need exact-tag approval.
23935
+ serve :11435; default-endpoint autostart optional. Docs: freellama://docs/index.`;
23936
+ var server = new McpServer(
23937
+ { name: "freellama", version: SERVER_VERSION },
23938
+ { instructions: INSTRUCTIONS }
23844
23939
  );
23845
- var transport = new StdioServerTransport();
23940
+ registerDocsResources(server);
23941
+ registerDoctorTool(server);
23942
+ registerSessionTool(server);
23943
+ registerModelsTool(server);
23944
+ registerScopeTool(server);
23945
+ registerRunTaskTool(server);
23946
+ registerTaskJobsTool(server);
23947
+ registerOllamaLifecycleTools(server);
23948
+ registerDelegateResearchTool(server);
23949
+ var transport = withoutSchemaDialect(new StdioServerTransport());
23846
23950
  await server.connect(transport);
23951
+ startServeInBackground();