@octocodeai/freellama-mcp-server 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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;
@@ -21684,35 +21676,159 @@ async function ensureServe(endpoint) {
21684
21676
  await starting;
21685
21677
  }
21686
21678
 
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;
21679
+ // src/lifecycle.ts
21680
+ var liveDelegates = /* @__PURE__ */ new Set();
21681
+ var DETACH_DELEGATES = process.platform !== "win32";
21682
+ function killDelegateTree(child2) {
21683
+ if (DETACH_DELEGATES && child2.pid !== void 0) {
21706
21684
  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;
21685
+ process.kill(-child2.pid, "SIGKILL");
21686
+ return;
21711
21687
  } catch {
21712
21688
  }
21713
21689
  }
21714
- return null;
21690
+ try {
21691
+ child2.kill("SIGKILL");
21692
+ } catch {
21693
+ }
21694
+ }
21695
+ function runDelegate(file, args, options) {
21696
+ const child2 = spawn2(file, args, { env: options.env, detached: DETACH_DELEGATES, stdio: ["ignore", "pipe", "pipe"] });
21697
+ const done = new Promise((resolve, reject) => {
21698
+ const out = [];
21699
+ const err = [];
21700
+ let outLen = 0;
21701
+ let errLen = 0;
21702
+ let failure;
21703
+ let timedOut = false;
21704
+ let settled = false;
21705
+ const fail = (error2) => {
21706
+ if (failure === void 0) failure = error2;
21707
+ killDelegateTree(child2);
21708
+ };
21709
+ const timer = setTimeout(() => {
21710
+ timedOut = true;
21711
+ killDelegateTree(child2);
21712
+ }, options.timeoutMs);
21713
+ const onAbort = () => {
21714
+ const abort = new Error("The operation was aborted", { cause: options.signal?.reason });
21715
+ abort.name = "AbortError";
21716
+ abort.code = "ABORT_ERR";
21717
+ fail(abort);
21718
+ };
21719
+ if (options.signal?.aborted) onAbort();
21720
+ else options.signal?.addEventListener("abort", onAbort, { once: true });
21721
+ const collect = (chunks, name) => (chunk) => {
21722
+ const length = name === "stdout" ? outLen += chunk.length : errLen += chunk.length;
21723
+ if (length > options.maxBuffer) {
21724
+ const overflow = new RangeError(`${name} maxBuffer length exceeded`);
21725
+ overflow.code = "ERR_CHILD_PROCESS_STDIO_MAXBUFFER";
21726
+ fail(overflow);
21727
+ return;
21728
+ }
21729
+ chunks.push(chunk);
21730
+ };
21731
+ child2.stdout.on("data", collect(out, "stdout"));
21732
+ child2.stderr.on("data", collect(err, "stderr"));
21733
+ const finish = (code, signal, spawnError) => {
21734
+ if (settled) return;
21735
+ settled = true;
21736
+ clearTimeout(timer);
21737
+ options.signal?.removeEventListener("abort", onAbort);
21738
+ const stdout = Buffer.concat(out).toString("utf8");
21739
+ const stderr = Buffer.concat(err).toString("utf8");
21740
+ if (spawnError ?? failure) {
21741
+ reject(spawnError ?? failure);
21742
+ return;
21743
+ }
21744
+ if (code === 0 && signal === null) {
21745
+ resolve({ stdout, stderr });
21746
+ return;
21747
+ }
21748
+ const error2 = new Error(`Command failed: ${[file, ...args].join(" ")}
21749
+ ${stderr}`);
21750
+ Object.assign(error2, { code, killed: timedOut, signal, stdout, stderr });
21751
+ reject(error2);
21752
+ };
21753
+ child2.once("error", (error2) => finish(null, null, error2));
21754
+ child2.once("close", (code, signal) => finish(code, signal));
21755
+ });
21756
+ return { child: child2, done };
21757
+ }
21758
+ function killLiveDelegates() {
21759
+ for (const child2 of liveDelegates) killDelegateTree(child2);
21760
+ liveDelegates.clear();
21761
+ }
21762
+ function installProcessLifecycle() {
21763
+ for (const stream of [process.stdout, process.stderr]) {
21764
+ stream.on("error", (error2) => {
21765
+ if (error2.code === "EPIPE") process.exit(0);
21766
+ throw error2;
21767
+ });
21768
+ }
21769
+ process.on("exit", () => {
21770
+ killLiveDelegates();
21771
+ stopAutostartedServe();
21772
+ });
21773
+ process.stdin.on("close", () => {
21774
+ killLiveDelegates();
21775
+ stopAutostartedServe();
21776
+ process.exit(0);
21777
+ });
21778
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
21779
+ process.on(signal, () => {
21780
+ killLiveDelegates();
21781
+ process.exit(0);
21782
+ });
21783
+ }
21715
21784
  }
21785
+
21786
+ // src/schema-dialect.ts
21787
+ function stripSchemaDialect(message) {
21788
+ const tools = message.result?.tools;
21789
+ if (!Array.isArray(tools)) return message;
21790
+ for (const tool of tools) {
21791
+ delete tool.inputSchema?.$schema;
21792
+ delete tool.outputSchema?.$schema;
21793
+ }
21794
+ return message;
21795
+ }
21796
+ function withoutSchemaDialect(transport2) {
21797
+ const send = transport2.send.bind(transport2);
21798
+ transport2.send = (message, options) => send(stripSchemaDialect(message), options);
21799
+ return transport2;
21800
+ }
21801
+
21802
+ // src/docs-resources.ts
21803
+ import { existsSync as existsSync3, readdirSync } from "node:fs";
21804
+ import { readFile } from "node:fs/promises";
21805
+ import path3 from "node:path";
21806
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
21807
+ function registerDocsResources(server2) {
21808
+ const PACKAGED_DOCS_DIR = path3.resolve(path3.dirname(fileURLToPath3(import.meta.url)), "..", "docs");
21809
+ if (!existsSync3(path3.join(PACKAGED_DOCS_DIR, "README.md"))) {
21810
+ throw new Error(`FreeLlama MCP documentation index is missing at ${PACKAGED_DOCS_DIR}/README.md; run the package build.`);
21811
+ }
21812
+ const packagedDocs = readdirSync(PACKAGED_DOCS_DIR).filter((name) => name.endsWith(".md")).sort();
21813
+ for (const name of packagedDocs) {
21814
+ const isIndex = name === "README.md";
21815
+ const slug = isIndex ? "index" : name.replace(/\.md$/, "");
21816
+ const uri = `freellama://docs/${slug}`;
21817
+ server2.registerResource(
21818
+ `freellama-docs-${slug.toLowerCase()}`,
21819
+ uri,
21820
+ {
21821
+ mimeType: "text/markdown",
21822
+ description: isIndex ? "Index of packaged FreeLlama operator and agent documentation. Read this first, then fetch one relevant document." : `Packaged FreeLlama documentation: ${name}.`
21823
+ },
21824
+ async () => ({
21825
+ contents: [{ uri, mimeType: "text/markdown", text: await readFile(path3.join(PACKAGED_DOCS_DIR, name), "utf8") }]
21826
+ })
21827
+ );
21828
+ }
21829
+ }
21830
+
21831
+ // src/helpers.ts
21716
21832
  async function ollamaFetch(endpoint, path6, init = {}) {
21717
21833
  const base = (endpoint ?? DEFAULT_OLLAMA_ENDPOINT).replace(/\/$/, "");
21718
21834
  const controller = new AbortController();
@@ -21759,62 +21875,6 @@ async function readNdjson(body, onLine) {
21759
21875
  }
21760
21876
  return text + decoder.decode();
21761
21877
  }
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
21878
  var endpointParam = external_exports.string().min(1).optional();
21819
21879
  var ollamaEndpointParam = external_exports.string().min(1).optional();
21820
21880
  var TASK_KINDS = [
@@ -21835,19 +21895,7 @@ function canonicalTaskKind(task) {
21835
21895
  var objectiveParam = external_exports.enum(["fastest", "balanced", "quality"]).optional();
21836
21896
  var executionPreferenceParam = external_exports.enum(["auto", "prefer_cpu", "prefer_gpu"]).optional();
21837
21897
  var minPlacementEvidenceParam = external_exports.enum(["configured", "observed"]).optional();
21838
- var CONFIDENCE_RANK = { low: 1, medium: 2 };
21839
21898
  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
21899
  var REQUIRED_CAPABILITIES = [
21852
21900
  "completion",
21853
21901
  "tools",
@@ -21880,7 +21928,8 @@ function taskMessages({ systemPrompt, messages, prompt, images }) {
21880
21928
  return [{ role: "system", content: systemPrompt }, ...conversation];
21881
21929
  }
21882
21930
  var batchTaskParam = external_exports.object({
21883
- task: taskParam.removeDefault().optional().describe("caller owns prompts and output format."),
21931
+ // Nested `task` shares the top-level description; repeating it re-sends the text every request.
21932
+ task: taskParam.removeDefault().optional(),
21884
21933
  objective: objectiveParam,
21885
21934
  model: external_exports.string().min(1).optional(),
21886
21935
  sessionId: external_exports.string().uuid().optional(),
@@ -21893,6 +21942,7 @@ var batchTaskParam = external_exports.object({
21893
21942
  priority: external_exports.enum(["interactive", "normal", "background"]).optional(),
21894
21943
  maxWaitSeconds: external_exports.number().int().positive().optional(),
21895
21944
  timeoutSeconds: external_exports.number().int().positive().optional(),
21945
+ stopOnRepetition: external_exports.boolean().optional().describe("Stream and stop once output repeats itself; keeps all generated text"),
21896
21946
  prompt: external_exports.string().min(1).optional(),
21897
21947
  systemPrompt: systemPromptParam,
21898
21948
  messages: messagesParam,
@@ -21907,56 +21957,27 @@ var batchTaskParam = external_exports.object({
21907
21957
  topLogprobs: external_exports.number().int().nonnegative().optional(),
21908
21958
  minConfidence: minConfidenceParam
21909
21959
  }).strict();
21960
+ function fresh(schema) {
21961
+ const def = { ...schema._def };
21962
+ for (const [key, value] of Object.entries(def)) {
21963
+ if (value instanceof external_exports.ZodType) def[key] = fresh(value);
21964
+ else if (Array.isArray(value) && value.length > 0 && value.every((v) => v instanceof external_exports.ZodType)) {
21965
+ def[key] = value.map((v) => fresh(v));
21966
+ }
21967
+ }
21968
+ if (schema instanceof external_exports.ZodObject) {
21969
+ const shape = Object.fromEntries(Object.entries(schema.shape).map(([k, v]) => [k, fresh(v)]));
21970
+ def.shape = () => shape;
21971
+ }
21972
+ return new schema.constructor(def);
21973
+ }
21974
+ var BATCH_TASK_FIELDS = new Set(Object.keys(batchTaskParam.shape));
21910
21975
  var batchItemParam = external_exports.object({
21911
21976
  id: external_exports.string().min(1),
21912
21977
  independent: external_exports.literal(true),
21913
- task: batchTaskParam
21978
+ task: fresh(batchTaskParam)
21914
21979
  }).strict();
21915
21980
  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
21981
  function configuredExternalCost(env = process.env) {
21961
21982
  const model = env.FREELLAMA_EXTERNAL_COST_MODEL;
21962
21983
  const inputRaw = env.FREELLAMA_EXTERNAL_COST_INPUT_USD_PER_M;
@@ -22013,11 +22034,6 @@ function clipText(text, limit) {
22013
22034
  const head = Math.floor(usable * 2 / 3);
22014
22035
  return text.slice(0, head) + marker + text.slice(-(usable - head));
22015
22036
  }
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
22037
  function resultSummary(value) {
22022
22038
  const explicit = value.summary;
22023
22039
  if (typeof explicit === "string" && explicit.trim()) return clipText(explicit.trim(), 500);
@@ -22036,7 +22052,7 @@ function taskAnswerText(payload) {
22036
22052
  const message = record2(response.message);
22037
22053
  if (message) {
22038
22054
  const parts = [];
22039
- if (typeof message.content === "string" && message.content.trim()) parts.push(message.content.trim());
22055
+ if (typeof message.content === "string" && message.content.trim()) parts.push(answerContent(message.content, payload));
22040
22056
  if (Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {
22041
22057
  parts.push(`tool_calls: ${JSON.stringify(message.tool_calls)}`);
22042
22058
  }
@@ -22052,6 +22068,15 @@ function taskAnswerText(payload) {
22052
22068
  }
22053
22069
  return void 0;
22054
22070
  }
22071
+ function answerContent(content, payload) {
22072
+ const check2 = record2(payload.output_check);
22073
+ const keep = check2?.unique_prefix_chars;
22074
+ if (check2?.repetition_loop !== true || typeof keep !== "number") return content.trim();
22075
+ const characters = Array.from(content);
22076
+ const trimmed = characters.slice(0, keep).join("").trim();
22077
+ return `${trimmed}
22078
+ [freellama: trimmed ${characters.length - keep} repeated characters; the model looped until num_predict \u2014 treat as low confidence]`;
22079
+ }
22055
22080
  function batchAnswerText(payload) {
22056
22081
  if (!Array.isArray(payload.results)) return void 0;
22057
22082
  const perItem = Math.max(512, Math.floor(ANSWER_TEXT_MAX_CHARS / Math.max(payload.results.length, 1)));
@@ -22067,7 +22092,7 @@ ${clipText(answer, perItem)}`;
22067
22092
  return lines.join("\n\n");
22068
22093
  }
22069
22094
  function structuredResult(value, options = {}) {
22070
- const text = options.text ?? (options.legacyJson ? serialize(value) : resultSummary(value));
22095
+ const text = options.text ?? resultSummary(value);
22071
22096
  return {
22072
22097
  content: [{ type: "text", text }],
22073
22098
  structuredContent: value
@@ -22093,6 +22118,19 @@ ${clipText(raw, 2e3)}`
22093
22118
  function isStructuredSuccess(result) {
22094
22119
  return !("isError" in result && result.isError) && "structuredContent" in result && result.structuredContent !== void 0;
22095
22120
  }
22121
+ var escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22122
+ var SERVE_ADDRESS_PATTERN = (() => {
22123
+ let url;
22124
+ try {
22125
+ url = new URL(DEFAULT_SERVE_ENDPOINT);
22126
+ } catch {
22127
+ return /_freellama/i;
22128
+ }
22129
+ const { hostname: hostname2, port } = url;
22130
+ const hosts = ["127.0.0.1", "localhost"].includes(hostname2) ? ["127.0.0.1", "localhost"] : [hostname2];
22131
+ const addresses = hosts.map((host) => escapeRegExp(port ? `${host}:${port}` : host));
22132
+ return new RegExp(`(?:_freellama|${addresses.join("|")})`, "i");
22133
+ })();
22096
22134
  function errorResult(error2) {
22097
22135
  const message = error2 instanceof Error ? error2.message : String(error2);
22098
22136
  let receipt;
@@ -22103,7 +22141,7 @@ function errorResult(error2) {
22103
22141
  }
22104
22142
  } catch {
22105
22143
  }
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);
22144
+ const managedServeUnavailable = /(?:connection refused|connect error|failed to connect|error sending request)/i.test(message) && SERVE_ADDRESS_PATTERN.test(message);
22107
22145
  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
22146
  const actionableMessage = managedServeUnavailable ? `${message}
22109
22147
 
@@ -22116,37 +22154,6 @@ Ollama is not running at that address. Ask the user to start Ollama (the app, or
22116
22154
  ...receipt ? { structuredContent: receipt } : {}
22117
22155
  };
22118
22156
  }
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
22157
  function summarizeEmbeddings(payload) {
22151
22158
  const response = payload.response;
22152
22159
  if (response === null || typeof response !== "object") return null;
@@ -22168,24 +22175,168 @@ function summarizeEmbeddings(payload) {
22168
22175
  };
22169
22176
  }
22170
22177
 
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) {
22187
- const tags = /<\/?div\b[^>]*>/gi;
22188
- let start;
22178
+ // src/tools/doctor.ts
22179
+ var doctorResultSchema = external_exports.object({
22180
+ summary: external_exports.string().optional(),
22181
+ endpoint: external_exports.string().optional()
22182
+ }).passthrough();
22183
+ function registerDoctorTool(server2) {
22184
+ server2.registerTool(
22185
+ "doctor",
22186
+ {
22187
+ 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.",
22188
+ inputSchema: external_exports.object({
22189
+ ollamaEndpoint: ollamaEndpointParam,
22190
+ serveEndpoint: endpointParam,
22191
+ view: external_exports.enum(["summary", "full", "status", "usage"]).optional(),
22192
+ days: external_exports.number().int().min(1).max(366).optional()
22193
+ }).strict(),
22194
+ outputSchema: doctorResultSchema,
22195
+ annotations: { readOnlyHint: true }
22196
+ },
22197
+ async ({ ollamaEndpoint, serveEndpoint, view, days }) => {
22198
+ try {
22199
+ if (days !== void 0 && view !== "usage") return errorResult(new Error("days is only valid with view: usage."));
22200
+ if (view === "status") return parsedResult(await status(serveEndpoint));
22201
+ if (view === "usage") return parsedResult(await usage(serveEndpoint, days));
22202
+ const report = parsedResult(await doctor(ollamaEndpoint));
22203
+ if (!isStructuredSuccess(report)) return report;
22204
+ try {
22205
+ report.structuredContent.machine = JSON.parse(await machine(serveEndpoint));
22206
+ delete report.structuredContent.machine_unavailable;
22207
+ } catch (error2) {
22208
+ if (report.structuredContent.machine == null) {
22209
+ report.structuredContent.machine_unavailable = `freellama serve unreachable, so no machine profile: ${error2 instanceof Error ? error2.message : String(error2)}`;
22210
+ }
22211
+ }
22212
+ try {
22213
+ report.structuredContent.platform_health = JSON.parse(await health(serveEndpoint));
22214
+ } catch (error2) {
22215
+ report.structuredContent.platform_health_unavailable = `freellama serve unreachable: ${error2 instanceof Error ? error2.message : String(error2)}`;
22216
+ }
22217
+ const full = { ...report.structuredContent };
22218
+ delete full.ollama_env_config;
22219
+ if ((view ?? "summary") === "full") return structuredResult(full);
22220
+ const running = full.running?.models ?? [];
22221
+ const compact = {
22222
+ status: "ok",
22223
+ summary: `Ollama ${String(full.version?.version ?? "unknown")}; ${running.length} resident model(s).`,
22224
+ endpoint: full.endpoint,
22225
+ ollama: { endpoint: full.endpoint, version: full.version, resident_model_count: running.length },
22226
+ machine: full.machine,
22227
+ local_conservative_config_posture: full.local_conservative_config_posture,
22228
+ host_runtime_signals: full.host_runtime_signals,
22229
+ evidence: { runtime: "ollama_api_version_and_ps", configuration: full.ollama_env_config_source },
22230
+ next: 'Use models{view:"installed"} to select a local model; view:"full" has Ollama configuration and admission.'
22231
+ };
22232
+ return structuredResult(compact);
22233
+ } catch (error2) {
22234
+ return errorResult(error2);
22235
+ }
22236
+ }
22237
+ );
22238
+ }
22239
+
22240
+ // src/serve-client.ts
22241
+ var createSession2 = withServe(createSession);
22242
+ var deleteSession2 = withServe(deleteSession);
22243
+ var killSession2 = withServe(killSession);
22244
+ var listModels2 = withServe(listModels);
22245
+ var route2 = withServe(route);
22246
+ var runTaskRequest2 = withServe(runTaskRequest);
22247
+ var runTaskBatchRequest2 = withServe(runTaskBatchRequest);
22248
+ var warmModelRequest2 = withServe(warmModelRequest);
22249
+ var listTaskJobs2 = withServe(listTaskJobs);
22250
+ var getTaskJob2 = withServe(getTaskJob);
22251
+ var cancelTaskJob2 = withServe(cancelTaskJob);
22252
+ var removeTaskJob2 = withServe(removeTaskJob);
22253
+ var createScope2 = withServe(createScope);
22254
+ var getScope2 = withServe(getScope);
22255
+ var forkScope2 = withServe(forkScope);
22256
+ var deleteScope2 = withServe(deleteScope);
22257
+
22258
+ // src/tools/session.ts
22259
+ var sessionResultSchema = external_exports.object({
22260
+ session_id: external_exports.string().optional(),
22261
+ deleted: external_exports.boolean().optional(),
22262
+ killed: external_exports.boolean().optional(),
22263
+ runner_stop_confirmed: external_exports.boolean().optional()
22264
+ }).passthrough();
22265
+ function registerSessionTool(server2) {
22266
+ server2.registerTool(
22267
+ "session",
22268
+ {
22269
+ 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.",
22270
+ inputSchema: external_exports.object({
22271
+ action: external_exports.enum(["create", "delete", "kill"]),
22272
+ sessionId: external_exports.string().uuid().optional(),
22273
+ endpoint: endpointParam
22274
+ }).strict(),
22275
+ outputSchema: sessionResultSchema,
22276
+ annotations: { destructiveHint: true }
22277
+ },
22278
+ async ({ action, sessionId, endpoint }) => {
22279
+ try {
22280
+ if (action === "create") {
22281
+ if (sessionId !== void 0) return errorResult(new Error("sessionId is only valid for action: delete or kill."));
22282
+ return structuredResult(JSON.parse(await createSession2(endpoint)));
22283
+ }
22284
+ if (sessionId === void 0) return errorResult(new Error(`action: ${action} requires sessionId.`));
22285
+ if (action === "kill") return parsedResult(await killSession2(endpoint, sessionId));
22286
+ await deleteSession2(endpoint, sessionId);
22287
+ return structuredResult({ session_id: sessionId, deleted: true });
22288
+ } catch (error2) {
22289
+ return errorResult(error2);
22290
+ }
22291
+ }
22292
+ );
22293
+ }
22294
+
22295
+ // src/paging.ts
22296
+ import { createHash } from "node:crypto";
22297
+ function pageLiveList(items, limit, cursor, identity) {
22298
+ const pageSize = limit ?? 20;
22299
+ const fingerprint = createHash("sha256").update(items.map(identity).join("\n")).digest("base64url").slice(0, 16);
22300
+ let offset = 0;
22301
+ if (cursor) {
22302
+ try {
22303
+ const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
22304
+ if (!Number.isInteger(parsed.offset) || parsed.offset < 0 || parsed.fingerprint !== fingerprint) {
22305
+ throw new Error("invalid or stale cursor");
22306
+ }
22307
+ offset = parsed.offset;
22308
+ } catch {
22309
+ throw new Error("`cursor` is invalid or the model list changed; restart without cursor.");
22310
+ }
22311
+ }
22312
+ const page = items.slice(offset, offset + pageSize);
22313
+ const nextOffset = offset + page.length;
22314
+ return {
22315
+ items: page,
22316
+ returned: page.length,
22317
+ total: items.length,
22318
+ next_cursor: nextOffset < items.length ? Buffer.from(JSON.stringify({ offset: nextOffset, fingerprint }), "utf8").toString("base64url") : null
22319
+ };
22320
+ }
22321
+
22322
+ // src/model-search.ts
22323
+ function decodeHtml(text) {
22324
+ const entities = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " ", middot: "\xB7", bull: "\u2022" };
22325
+ return text.replace(/&(#x[\da-f]+|#\d+|[a-z]+);/gi, (raw, entity) => {
22326
+ if (!entity.startsWith("#")) return entities[entity.toLowerCase()] ?? raw;
22327
+ const code = entity[1].toLowerCase() === "x" ? Number.parseInt(entity.slice(2), 16) : Number(entity.slice(1));
22328
+ return code > 0 && code <= 1114111 && !(code >= 55296 && code <= 57343) ? String.fromCodePoint(code) : raw;
22329
+ });
22330
+ }
22331
+ function attribute(tag, name) {
22332
+ return tag.match(new RegExp(`(?:^|\\s)${name}\\s*=\\s*(["'])([\\s\\S]*?)\\1`, "i"))?.[2];
22333
+ }
22334
+ function htmlText(html) {
22335
+ 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");
22336
+ }
22337
+ function divContent(html, id) {
22338
+ const tags = /<\/?div\b[^>]*>/gi;
22339
+ let start;
22189
22340
  let depth = 0;
22190
22341
  for (const match of html.matchAll(tags)) {
22191
22342
  if (start === void 0) {
@@ -22227,7 +22378,8 @@ function parseModelSearch(html) {
22227
22378
  const name = link?.[1] ?? link?.[2];
22228
22379
  if (!name) continue;
22229
22380
  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() ?? "";
22381
+ const rawDescription = block.match(/<p class="max-w-lg[^"]*">([\s\S]*?)<\/p>/)?.[1];
22382
+ const description = rawDescription === void 0 ? "" : decodeHtml(rawDescription.replace(/<[^>]+>/g, "")).replace(/\s+/g, " ").trim();
22231
22383
  const capabilities = [...block.matchAll(/text-(?:indigo-600|cyan-500)[^>]*>([a-z]+)<\/span>/g)].map(
22232
22384
  (m) => m[1]
22233
22385
  );
@@ -22240,8 +22392,7 @@ function parseModelSearch(html) {
22240
22392
  capabilities,
22241
22393
  pulls: stat("Pulls"),
22242
22394
  tags: stat("Tag"),
22243
- cloudAvailable: capabilities.includes("cloud"),
22244
- cloudOnly: null
22395
+ cloudAvailable: capabilities.includes("cloud")
22245
22396
  });
22246
22397
  }
22247
22398
  return results;
@@ -22284,6 +22435,10 @@ var CACHE_TTL_MS = 60 * 60 * 1e3;
22284
22435
  var FAILURE_TTL_MS = 60 * 1e3;
22285
22436
  var MAX_PAGE_BYTES = 2 * 1024 * 1024;
22286
22437
  var MAX_README_CHARS = 64 * 1024;
22438
+ var errorMessage = (error2) => error2 instanceof Error ? error2.message : String(error2);
22439
+ function isRemoteModel(entry, name) {
22440
+ return Boolean(entry.remote_host || entry.remote_model) || /-cloud$/i.test(name);
22441
+ }
22287
22442
  function libraryReference(model) {
22288
22443
  const match = model.match(/^(?:library\/)?([a-zA-Z0-9][\w.-]*(?:\/[a-zA-Z0-9][\w.-]*)?)(?::([\w.-]+))?$/);
22289
22444
  if (!match || match[1].split("/").some((part) => part === "." || part === "..")) return null;
@@ -22402,7 +22557,7 @@ var OllamaLibraryClient = class {
22402
22557
  } catch (error2) {
22403
22558
  result = {
22404
22559
  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."
22560
+ message: error2 instanceof LibraryFetchError ? error2.message : `Ollama library lookup failed or timed out (${errorMessage(error2)}). Local metadata remains available.`
22406
22561
  };
22407
22562
  }
22408
22563
  const now = (this.options.now ?? Date.now)();
@@ -22481,7 +22636,7 @@ function modelKnowledge(model, data, record3, includeReadme = false, supportedFe
22481
22636
  quality: "metadata_only",
22482
22637
  note: "Supported features and advertised uses do not establish task quality. Keep exact-model benchmark and policy evidence separate."
22483
22638
  };
22484
- if (data.remote_host || data.remote_model || /-cloud$/i.test(model)) {
22639
+ if (isRemoteModel(data, model)) {
22485
22640
  return { ...local, localModel: false, library: { status: "remote_model", message: "This Ollama entry points to a remote model." } };
22486
22641
  }
22487
22642
  if (!record3) return { ...local, localModel: true, library: { status: "not_requested" } };
@@ -22531,11 +22686,11 @@ async function enrichInstalledModels(models, client, includeLibrary, loadTags) {
22531
22686
  local = { ...model, ...entry };
22532
22687
  if (Array.isArray(entry.capabilities)) featureSource = "ollama_api";
22533
22688
  }
22534
- } catch {
22535
- localError = "The selected Ollama backend's inventory could not be verified; public guidance was withheld.";
22689
+ } catch (error2) {
22690
+ localError = `The selected Ollama backend's inventory could not be verified (${errorMessage(error2)}); public guidance was withheld.`;
22536
22691
  }
22537
22692
  }
22538
- const record3 = includeLibrary && !localError && !local.remote_host && !local.remote_model && !/-cloud$/i.test(name) ? await client.lookup(name) : void 0;
22693
+ const record3 = includeLibrary && !localError && !isRemoteModel(local, name) ? await client.lookup(name) : void 0;
22539
22694
  const knowledge = modelKnowledge(name, local, record3, false, featureSource);
22540
22695
  results[index] = { ...model, knowledge: localError ? { ...knowledge, library: { status: "unverified", message: localError } } : knowledge };
22541
22696
  }
@@ -22543,229 +22698,277 @@ async function enrichInstalledModels(models, client, includeLibrary, loadTags) {
22543
22698
  return results;
22544
22699
  }
22545
22700
 
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
- }
22701
+ // src/tools/models.ts
22702
+ var modelsResultSchema = external_exports.object({
22703
+ models: external_exports.array(external_exports.record(external_exports.unknown())).optional(),
22704
+ tags: external_exports.array(external_exports.record(external_exports.unknown())).optional(),
22705
+ page: external_exports.unknown().optional(),
22706
+ knowledge: external_exports.unknown().optional(),
22707
+ library: external_exports.unknown().optional()
22708
+ }).passthrough();
22709
+ var ollamaLibrary = new OllamaLibraryClient();
22710
+ var HOST_MEMORY_FIT_FRACTION = 0.6;
22711
+ async function libraryLookup({
22712
+ model,
22713
+ query,
22714
+ capabilities,
22715
+ order,
22716
+ limit,
22717
+ cursor,
22718
+ includeReadme,
22719
+ endpoint,
22720
+ ollamaEndpoint
22721
+ }) {
22722
+ const localState = async () => {
22723
+ let installed = null;
22724
+ let installedUnavailable;
22725
+ let memoryBytes = null;
22726
+ try {
22727
+ const tags = await ollamaFetch(ollamaEndpoint, "/api/tags");
22728
+ installed = new Set((tags.models ?? []).map((m) => m.name ?? ""));
22729
+ } catch (error2) {
22730
+ installedUnavailable = `Local inventory could not be read (${error2 instanceof Error ? error2.message : String(error2)}); installed is null.`;
22564
22731
  }
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
22732
  try {
22575
- if (!existsSync4(file)) continue;
22576
- const parsed = JSON.parse(readFileSync(file, "utf8"));
22577
- return parsed.models ?? {};
22733
+ const profile = JSON.parse(await machine(endpoint));
22734
+ memoryBytes = profile.memory_bytes ?? profile.unified_memory_bytes ?? null;
22578
22735
  } catch {
22579
22736
  }
22737
+ return { installed, installedUnavailable, memoryBytes };
22738
+ };
22739
+ if (!model && cursor !== void 0) {
22740
+ return errorResult(new Error("Library step 1 has no `cursor`; it pages step-2 tags. Use `limit` for search, or pass `model`."));
22580
22741
  }
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
- };
22742
+ try {
22743
+ if (model) {
22744
+ const ref = libraryReference(model);
22745
+ if (!ref) throw new Error("Use an exact Ollama family or namespace/model name, not a URL or path.");
22746
+ const { family } = ref;
22747
+ const library = await ollamaLibrary.lookup(model);
22748
+ if (library.status !== "available") return structuredResult({
22749
+ family,
22750
+ sourcePage: ref.url,
22751
+ tags: [],
22752
+ tagsUnavailable: library.message,
22753
+ library,
22754
+ recommendation: null
22755
+ });
22756
+ const tags = library.tags;
22757
+ const { installed: installed2, installedUnavailable: installedUnavailable2, memoryBytes } = await localState();
22758
+ const budget = memoryBytes ? memoryBytes * HOST_MEMORY_FIT_FRACTION : null;
22759
+ const annotated = tags.map((t) => ({
22760
+ ...t,
22761
+ installed: installed2 ? installed2.has(t.tag) : null,
22762
+ fitsInMemory: budget && t.sizeBytes ? t.sizeBytes <= budget : null,
22763
+ fitScope: "host_memory_budget_only"
22764
+ }));
22765
+ const runnable = annotated.filter((t) => t.fitsInMemory === true && !t.cloud);
22766
+ const isEmbeddingFamily = library.card.capabilities.includes("embedding");
22767
+ const sized = runnable.filter((t) => t.sizeBytes);
22768
+ 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;
22769
+ const tagPage = pageLiveList(annotated, limit, cursor, (tag) => tag.tag);
22770
+ return structuredResult({
22771
+ family,
22772
+ sourcePage: ref.url,
22773
+ library: {
22774
+ status: library.status,
22775
+ sourceUrl: library.sourceUrl,
22776
+ tagsSourceUrl: library.tagsSourceUrl,
22777
+ fetchedAt: library.fetchedAt,
22778
+ expiresAt: library.expiresAt,
22779
+ cached: library.cached,
22780
+ ...publicModelGuidance(library, void 0, includeReadme)
22781
+ },
22782
+ ...installedUnavailable2 ? { installedUnavailable: installedUnavailable2 } : {},
22783
+ machineMemoryBytes: memoryBytes,
22784
+ fitBudgetBytes: budget,
22785
+ tags: tagPage.items,
22786
+ page: { returned: tagPage.returned, total: tagPage.total, next_cursor: tagPage.next_cursor },
22787
+ 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.",
22788
+ recommendation: best ? libraryTrialCandidate(best.tag, isEmbeddingFamily) : null
22789
+ });
22790
+ }
22791
+ const params = new URLSearchParams();
22792
+ for (const c of capabilities ?? []) params.append("c", c);
22793
+ if (query) params.set("q", query);
22794
+ if (order === "newest") params.set("o", "newest");
22795
+ const search = await ollamaLibrary.search(params);
22796
+ const url = search.url;
22797
+ const parsed = search.models.slice(0, limit ?? 10);
22798
+ const { installed, installedUnavailable } = await localState();
22799
+ const installedFamilies = installed ? new Set([...installed].map((n) => n.split(":")[0])) : null;
22800
+ return structuredResult({
22801
+ query: url,
22802
+ fetchedAt: search.fetchedAt,
22803
+ ...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." } : {},
22804
+ order: order ?? "popular",
22805
+ count: parsed.length,
22806
+ ...installedUnavailable ? { installedUnavailable } : {},
22807
+ nextStep: 'Not pullable yet. Call again with view:"library", model:"<name>" to get tags, sizes and memory fit.',
22808
+ models: parsed.map((m) => ({ ...m, installed: installedFamilies ? installedFamilies.has(m.name) : null }))
22809
+ });
22810
+ } catch (error2) {
22811
+ return errorResult(error2);
22626
22812
  }
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
22813
  }
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
22814
+ function registerModelsTool(server2) {
22815
+ server2.registerTool(
22816
+ "models",
22817
+ {
22818
+ 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.",
22819
+ inputSchema: external_exports.object({
22820
+ view: external_exports.enum(["installed", "resident", "detail", "raw", "library"]).optional(),
22821
+ model: external_exports.string().min(1).optional(),
22822
+ includeVerbose: external_exports.boolean().optional(),
22823
+ includeLibrary: external_exports.boolean().optional(),
22824
+ includeReadme: external_exports.boolean().optional(),
22825
+ query: external_exports.string().min(1).optional(),
22826
+ capabilities: external_exports.array(external_exports.enum(["vision", "tools", "thinking", "embedding", "cloud"])).min(1).max(5).optional(),
22827
+ order: external_exports.enum(["popular", "newest"]).optional(),
22828
+ limit: external_exports.number().int().positive().max(50).optional(),
22829
+ cursor: external_exports.string().min(1).optional(),
22830
+ endpoint: endpointParam,
22831
+ ollamaEndpoint: ollamaEndpointParam
22832
+ }).strict(),
22833
+ outputSchema: modelsResultSchema,
22834
+ annotations: { readOnlyHint: true }
22672
22835
  },
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
22836
+ async ({ view, model, includeVerbose, includeLibrary, includeReadme, query, capabilities, order, limit, cursor, endpoint, ollamaEndpoint }) => {
22837
+ try {
22838
+ const selectedView = view ?? "installed";
22839
+ if (includeLibrary !== void 0 && selectedView !== "installed" && selectedView !== "detail") {
22840
+ return errorResult(new Error('`includeLibrary` is valid only for views "installed" and "detail".'));
22841
+ }
22842
+ if (includeReadme !== void 0 && !(selectedView === "library" && model || selectedView === "detail" && includeLibrary === true)) {
22843
+ return errorResult(new Error('`includeReadme` needs library step 2, or view "detail" with `includeLibrary:true`.'));
22844
+ }
22845
+ if (selectedView !== "library" && [query, capabilities, order].some((value) => value !== void 0)) {
22846
+ return errorResult(new Error(`view "${selectedView}" does not accept library search fields.`));
22847
+ }
22848
+ if (selectedView !== "library" && selectedView !== "raw" && [limit, cursor].some((value) => value !== void 0)) {
22849
+ return errorResult(new Error(`view "${selectedView}" accepts neither pagination nor library search fields.`));
22850
+ }
22851
+ if (selectedView !== "detail" && includeVerbose !== void 0) {
22852
+ return errorResult(new Error('`includeVerbose` is valid only for view "detail".'));
22853
+ }
22854
+ if (selectedView !== "detail" && selectedView !== "library" && model !== void 0) {
22855
+ return errorResult(new Error('`model` is valid only for views "detail" and "library".'));
22856
+ }
22857
+ if (selectedView === "library" && model && [query, capabilities, order].some((value) => value !== void 0)) {
22858
+ return errorResult(
22859
+ new Error("Library step 2 accepts only `model` plus endpoint overrides; omit step-1 search fields.")
22860
+ );
22861
+ }
22862
+ switch (selectedView) {
22863
+ case "raw": {
22864
+ const raw = await ollamaFetch(ollamaEndpoint, "/api/tags");
22865
+ const models = Array.isArray(raw.models) ? raw.models : [];
22866
+ const page = pageLiveList(models, limit, cursor, (model2) => String(model2.name ?? JSON.stringify(model2)));
22867
+ return structuredResult({ ...raw, models: page.items, page: { returned: page.returned, total: page.total, next_cursor: page.next_cursor } });
22868
+ }
22869
+ case "resident": {
22870
+ const managed = parsedResult(await listModels2(endpoint));
22871
+ if (!isStructuredSuccess(managed)) return managed;
22872
+ const data = managed.structuredContent;
22873
+ const models = (data.models ?? []).filter((entry) => entry.resident === true).map((entry) => {
22874
+ const { resident_view: view2, ...rest } = entry;
22875
+ if (!view2) return rest;
22876
+ return {
22877
+ ...rest,
22878
+ placement: {
22879
+ gpu_percent: view2.gpu_percent,
22880
+ assigned: view2.assigned,
22881
+ processor: view2.processor_label,
22882
+ ...view2.warning !== void 0 ? { warning: view2.warning } : {}
22883
+ }
22884
+ };
22885
+ });
22886
+ return structuredResult({ ...data, models });
22887
+ }
22888
+ case "detail": {
22889
+ if (!model) {
22890
+ return errorResult(new Error('view "detail" needs `model` set to an installed tag.'));
22891
+ }
22892
+ const data = await ollamaFetch(ollamaEndpoint, "/api/show", {
22893
+ method: "POST",
22894
+ body: { model }
22895
+ });
22896
+ let local = data;
22897
+ if (includeLibrary && !isRemoteModel(data, model)) {
22898
+ try {
22899
+ const inventory = await ollamaFetch(ollamaEndpoint, "/api/tags");
22900
+ const canonical = libraryReference(model)?.tag;
22901
+ const entry = canonical && inventory.models?.find((entry2) => libraryReference(String(entry2.name ?? entry2.model))?.tag === canonical);
22902
+ if (entry) local = { ...entry, ...data, digest: entry.digest };
22903
+ } catch {
22904
+ }
22905
+ }
22906
+ const library = includeLibrary && !isRemoteModel(local, model) ? await ollamaLibrary.lookup(model) : void 0;
22907
+ const { license, modelfile, ...rest } = data;
22908
+ const modelInfo = rest.model_info ?? {};
22909
+ const contextEntry = Object.entries(modelInfo).find(
22910
+ ([key, value]) => key.endsWith(".context_length") && typeof value === "number"
22911
+ );
22912
+ return structuredResult({
22913
+ ...rest,
22914
+ max_context_length: contextEntry?.[1] ?? null,
22915
+ knowledge: modelKnowledge(model, local, library, includeReadme),
22916
+ ...includeVerbose ? { license, modelfile } : {}
22917
+ });
22918
+ }
22919
+ case "library":
22920
+ return await libraryLookup({ model, query, capabilities, order, limit, cursor, includeReadme, endpoint, ollamaEndpoint });
22921
+ default: {
22922
+ const result = parsedResult(await listModels2(endpoint));
22923
+ if (!isStructuredSuccess(result)) return result;
22924
+ const data = result.structuredContent;
22925
+ return structuredResult({ ...data, models: await enrichInstalledModels(
22926
+ data.models ?? [],
22927
+ ollamaLibrary,
22928
+ includeLibrary ?? false,
22929
+ async (upstream) => (await ollamaFetch(upstream ?? ollamaEndpoint, "/api/tags")).models ?? []
22930
+ ) });
22931
+ }
22932
+ }
22933
+ } catch (error2) {
22934
+ return errorResult(error2);
22935
+ }
22678
22936
  }
22679
- };
22937
+ );
22680
22938
  }
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);
22939
+
22940
+ // src/tools/scope.ts
22941
+ function registerScopeTool(server2) {
22687
22942
  server2.registerTool("scope", {
22688
- description: "Use when: saved history. Do not use when: KV control. Returns: process-local scope metadata; history is opt-in.",
22943
+ 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
22944
  inputSchema: external_exports.object({
22690
22945
  action: external_exports.enum(["create", "get", "fork", "delete"]),
22691
22946
  endpoint: endpointParam,
22692
22947
  scopeId: external_exports.string().uuid().optional(),
22693
22948
  revision: external_exports.number().int().nonnegative().optional(),
22694
22949
  messages: messagesParam.unwrap().min(0).optional(),
22695
- routeDefaults: routeDefaults.optional(),
22696
- limits: limits.optional(),
22697
22950
  includeMessages: external_exports.boolean().optional()
22698
22951
  }).strict(),
22699
22952
  outputSchema: objectResultSchema,
22700
22953
  annotations: { destructiveHint: true }
22701
- }, async ({ action, endpoint, scopeId, revision, messages, routeDefaults: defaults, limits: bounds, includeMessages }) => {
22954
+ }, async ({ action, endpoint, scopeId, revision, messages, includeMessages }) => {
22702
22955
  try {
22703
22956
  if (action === "create" !== (scopeId === void 0)) throw new Error("scopeId is required for get/fork/delete, forbidden for create.");
22704
22957
  if (action === "fork" !== (revision !== void 0)) throw new Error("revision is required only for fork.");
22705
22958
  if (includeMessages !== void 0 && action !== "get") throw new Error("includeMessages is valid only for get.");
22706
22959
  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
22960
  if (action === "delete") {
22709
22961
  await deleteScope2(endpoint, scopeId);
22710
22962
  return structuredResult({ deleted: true, scope_id: scopeId });
22711
22963
  }
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
- }));
22964
+ return parsedResult(action === "create" ? await createScope2(endpoint, { messages }) : action === "fork" ? await forkScope2(endpoint, scopeId, { revision }) : await getScope2(endpoint, scopeId, includeMessages));
22749
22965
  } catch (error2) {
22750
22966
  return errorResult(error2);
22751
22967
  }
22752
22968
  });
22753
22969
  }
22754
22970
 
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);
22971
+ // src/telemetry.ts
22769
22972
  var EXTERNAL_COST = configuredExternalCost();
22770
22973
  function withTaskTelemetry(result) {
22771
22974
  if (!isStructuredSuccess(result)) return result;
@@ -22808,55 +23011,469 @@ function withBatchTelemetry(result) {
22808
23011
  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
23012
  }, { text: batchAnswerText(payload) });
22810
23013
  }
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) {
23014
+
23015
+ // src/tools/run-task.ts
23016
+ var taskResultSchema = external_exports.object({
23017
+ selected_model: external_exports.string().optional(),
23018
+ context_window_fit: external_exports.string().optional(),
23019
+ execution: external_exports.unknown().optional(),
23020
+ response: external_exports.unknown().optional(),
23021
+ telemetry: external_exports.unknown().optional()
23022
+ }).passthrough();
23023
+ var batchResultSchema = external_exports.object({
23024
+ results: external_exports.unknown().optional(),
23025
+ telemetry: external_exports.unknown().optional()
23026
+ }).passthrough();
23027
+ function validateTaskPayload(task, label) {
23028
+ const fail = (message) => label ? `${label}: ${message}` : message;
23029
+ if (task.scopeId === void 0 !== (task.scopeRevision === void 0)) {
23030
+ return fail("scopeId and scopeRevision must be supplied together.");
23031
+ }
23032
+ if (task.topLogprobs !== void 0 && task.logprobs !== true) return fail("`topLogprobs` requires `logprobs:true`.");
23033
+ if (task.task === "embedding") {
23034
+ if (task.input === void 0) return fail('task "embedding" requires `input`.');
23035
+ const chatControls = [
23036
+ task.prompt,
23037
+ task.systemPrompt,
23038
+ task.images,
23039
+ task.messages,
23040
+ task.tools,
23041
+ task.format,
23042
+ task.think,
23043
+ task.logprobs,
23044
+ task.topLogprobs
23045
+ ];
23046
+ if (chatControls.some((value) => value !== void 0)) {
23047
+ return fail('task "embedding" accepts `input`, `options`, and routing fields; chat controls are not valid.');
23048
+ }
23049
+ return null;
23050
+ }
23051
+ if (task.input !== void 0 || task.returnEmbeddings !== void 0) {
23052
+ return fail('`input` and `returnEmbeddings` are valid only for task "embedding".');
23053
+ }
23054
+ if (task.prompt === void 0 && task.systemPrompt === void 0 && (task.messages === void 0 || task.messages.length === 0)) {
23055
+ return fail(`task "${task.task ?? "completion"}" requires \`prompt\`, \`systemPrompt\`, or \`messages\`.`);
23056
+ }
23057
+ if (task.images !== void 0 && (task.messages !== void 0 || task.prompt === void 0)) {
23058
+ return fail("Top-level `images` is valid only with `prompt`; put images inside messages instead.");
23059
+ }
23060
+ return null;
23061
+ }
23062
+ async function runBatch(tasks, maxParallelism, endpoint) {
23063
+ for (const item of tasks) {
23064
+ const problem = validateTaskPayload(item.task, `batch item ${item.id}`);
23065
+ if (problem) return errorResult(new Error(problem));
23066
+ }
23067
+ return withBatchTelemetry(parsedResult(await runTaskBatchRequest2(endpoint ?? DEFAULT_SERVE_ENDPOINT, {
23068
+ tasks: tasks.map(({ id, independent, task }) => ({
23069
+ id,
23070
+ independent,
23071
+ task: {
23072
+ task: task.task === void 0 ? task.scopeId === void 0 ? "completion" : void 0 : canonicalTaskKind(task.task),
23073
+ objective: task.objective ?? (task.scopeId === void 0 ? "balanced" : void 0),
23074
+ model: task.model,
23075
+ session_id: task.sessionId,
23076
+ scope_id: task.scopeId,
23077
+ scope_revision: task.scopeRevision,
23078
+ context_tokens: task.contextTokens,
23079
+ execution_preference: task.executionPreference,
23080
+ min_placement_evidence: task.minPlacementEvidence,
23081
+ required_capabilities: task.requiredCapabilities,
23082
+ priority: task.priority ?? "normal",
23083
+ max_wait_seconds: task.maxWaitSeconds,
23084
+ timeout_seconds: task.timeoutSeconds,
23085
+ stop_on_repetition: task.stopOnRepetition,
23086
+ prompt: task.prompt,
23087
+ images: task.images,
23088
+ messages: taskMessages(task),
23089
+ input: task.input,
23090
+ tools: task.tools,
23091
+ keep_alive: task.keepAlive,
23092
+ min_confidence: task.minConfidence,
23093
+ request_options: {
23094
+ format: task.format,
23095
+ think: task.think,
23096
+ options: task.options,
23097
+ logprobs: task.logprobs,
23098
+ top_logprobs: task.topLogprobs
23099
+ }
23100
+ }
23101
+ })),
23102
+ max_parallelism: maxParallelism
23103
+ })));
23104
+ }
23105
+ function registerRunTaskTool(server2) {
23106
+ server2.registerTool(
23107
+ "run_task",
23108
+ {
23109
+ 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.",
23110
+ inputSchema: external_exports.object({
23111
+ endpoint: endpointParam,
23112
+ task: taskParam.removeDefault().optional().describe("caller owns prompts and output format."),
23113
+ objective: objectiveParam,
23114
+ model: external_exports.string().min(1).optional(),
23115
+ sessionId: external_exports.string().uuid().optional(),
23116
+ scopeId: external_exports.string().uuid().optional(),
23117
+ scopeRevision: external_exports.number().int().nonnegative().optional(),
23118
+ contextTokens: external_exports.number().int().positive().optional().describe("Total input + output window (num_ctx)."),
23119
+ executionPreference: executionPreferenceParam,
23120
+ minPlacementEvidence: minPlacementEvidenceParam,
23121
+ requiredCapabilities: requiredCapabilitiesParam,
23122
+ prompt: external_exports.string().min(1).optional(),
23123
+ systemPrompt: systemPromptParam,
23124
+ 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]"),
23125
+ messages: messagesParam.describe(
23126
+ "Caller system prompts; no injected task instructions. Overrides prompt; extra fields preserved."
23127
+ ),
23128
+ input: external_exports.union([external_exports.string().min(1), external_exports.array(external_exports.string().min(1)).min(1)]).optional(),
23129
+ tools: localToolsParam,
23130
+ keepAlive: external_exports.string().min(1).optional().describe('"0" unloads now, "-1" pins; omitted = adaptive finite TTL'),
23131
+ format: external_exports.union([external_exports.literal("json"), external_exports.record(external_exports.unknown())]).optional(),
23132
+ think: external_exports.union([external_exports.boolean(), external_exports.enum(["low", "medium", "high"])]).optional(),
23133
+ options: external_exports.record(external_exports.unknown()).optional().describe("num_ctx/contextTokens and num_gpu/placement are routing-owned; num_predict caps output."),
23134
+ logprobs: external_exports.boolean().optional(),
23135
+ topLogprobs: external_exports.number().int().nonnegative().optional(),
23136
+ minConfidence: minConfidenceParam,
23137
+ priority: external_exports.enum(["interactive", "normal", "background"]).optional(),
23138
+ maxWaitSeconds: external_exports.number().int().positive().optional(),
23139
+ timeoutSeconds: external_exports.number().int().positive().optional(),
23140
+ stopOnRepetition: external_exports.boolean().optional().describe("Stream and stop once output repeats itself (a loop to num_predict); keeps all generated text"),
23141
+ defer: external_exports.boolean().optional(),
23142
+ returnEmbeddings: external_exports.boolean().optional(),
23143
+ preview: external_exports.boolean().optional().describe(
23144
+ "true = routing fields only; rejects payloads and runtime controls; never executes"
23145
+ ),
23146
+ warm: external_exports.boolean().optional().describe("Load exact `model`; no prompt"),
23147
+ tasks: external_exports.array(batchItemParam).min(1).max(64).optional().describe("Batch mode: independent items; per-item fields go inside each item's `task`"),
23148
+ maxParallelism: external_exports.number().int().positive().max(64).optional().describe("Batch mode only")
23149
+ }).strict(),
23150
+ outputSchema: taskResultSchema.merge(batchResultSchema),
23151
+ annotations: { destructiveHint: false }
23152
+ },
23153
+ async ({
23154
+ endpoint,
23155
+ task,
23156
+ objective,
23157
+ model,
23158
+ sessionId,
23159
+ scopeId,
23160
+ scopeRevision,
23161
+ contextTokens,
23162
+ executionPreference,
23163
+ minPlacementEvidence,
23164
+ requiredCapabilities,
23165
+ prompt,
23166
+ systemPrompt,
23167
+ images,
23168
+ messages,
23169
+ input,
23170
+ tools,
23171
+ keepAlive,
23172
+ format,
23173
+ think,
23174
+ options,
23175
+ logprobs,
23176
+ topLogprobs,
23177
+ minConfidence,
23178
+ priority,
23179
+ maxWaitSeconds,
23180
+ timeoutSeconds,
23181
+ stopOnRepetition,
23182
+ defer,
23183
+ returnEmbeddings,
23184
+ preview,
23185
+ warm,
23186
+ tasks,
23187
+ maxParallelism
23188
+ }) => {
23189
+ try {
23190
+ const singleFields = {
23191
+ task,
23192
+ objective,
23193
+ model,
23194
+ sessionId,
23195
+ scopeId,
23196
+ scopeRevision,
23197
+ contextTokens,
23198
+ executionPreference,
23199
+ minPlacementEvidence,
23200
+ requiredCapabilities,
23201
+ prompt,
23202
+ systemPrompt,
23203
+ images,
23204
+ messages,
23205
+ input,
23206
+ tools,
23207
+ keepAlive,
23208
+ format,
23209
+ think,
23210
+ options,
23211
+ logprobs,
23212
+ topLogprobs,
23213
+ minConfidence,
23214
+ priority,
23215
+ maxWaitSeconds,
23216
+ timeoutSeconds,
23217
+ stopOnRepetition,
23218
+ defer,
23219
+ returnEmbeddings,
23220
+ preview,
23221
+ warm
23222
+ };
23223
+ if (tasks !== void 0) {
23224
+ const stray = Object.entries(singleFields).filter(([, value]) => value !== void 0).map(([name]) => name);
23225
+ const unsupported = stray.filter((name) => !BATCH_TASK_FIELDS.has(name)).map((name) => `\`${name}\``);
23226
+ if (unsupported.length > 0) {
23227
+ return errorResult(new Error(`${unsupported.join(", ")} ${unsupported.length === 1 ? "is" : "are"} not supported with \`tasks\`.`));
23228
+ }
23229
+ const movable = stray.map((name) => `\`${name}\``);
23230
+ if (movable.length > 0) {
23231
+ return errorResult(new Error(`With \`tasks\`, put ${movable.join(", ")} inside each item's \`task\`.`));
23232
+ }
23233
+ return await runBatch(tasks, maxParallelism, endpoint);
23234
+ }
23235
+ if (maxParallelism !== void 0) return errorResult(new Error("`maxParallelism` is valid only with `tasks`."));
23236
+ if (warm) {
23237
+ const allowed = /* @__PURE__ */ new Set([
23238
+ "warm",
23239
+ "model",
23240
+ "task",
23241
+ "contextTokens",
23242
+ "executionPreference",
23243
+ "minPlacementEvidence",
23244
+ "priority",
23245
+ "keepAlive",
23246
+ "maxWaitSeconds",
23247
+ "timeoutSeconds",
23248
+ "defer"
23249
+ ]);
23250
+ const stray = Object.entries(singleFields).filter(([name, value]) => value !== void 0 && !allowed.has(name)).map(([name]) => `\`${name}\``);
23251
+ if (stray.length > 0) return errorResult(new Error(`\`warm:true\` loads a model without generation; remove ${stray.join(", ")}.`));
23252
+ if (model === void 0) return errorResult(new Error("`warm:true` requires an exact installed `model`."));
23253
+ if (task === "embedding") return errorResult(new Error("`warm:true` supports chat models; embedding warming is unsupported."));
23254
+ return parsedResult(await warmModelRequest2(endpoint, {
23255
+ model,
23256
+ task: task === void 0 ? void 0 : canonicalTaskKind(task),
23257
+ context_tokens: contextTokens,
23258
+ execution_preference: executionPreference,
23259
+ min_placement_evidence: minPlacementEvidence,
23260
+ priority,
23261
+ keep_alive: keepAlive,
23262
+ max_wait_seconds: maxWaitSeconds,
23263
+ timeout_seconds: timeoutSeconds,
23264
+ defer
23265
+ }));
23266
+ }
23267
+ const canonicalTask = canonicalTaskKind(task ?? "completion");
23268
+ if (preview) {
23269
+ const executionOnlyFields = [
23270
+ ["scopeId", scopeId],
23271
+ ["scopeRevision", scopeRevision],
23272
+ ["prompt", prompt],
23273
+ ["systemPrompt", systemPrompt],
23274
+ ["images", images],
23275
+ ["messages", messages],
23276
+ ["input", input],
23277
+ ["tools", tools],
23278
+ ["keepAlive", keepAlive],
23279
+ ["format", format],
23280
+ ["think", think],
23281
+ ["options", options],
23282
+ ["logprobs", logprobs],
23283
+ ["topLogprobs", topLogprobs],
23284
+ ["returnEmbeddings", returnEmbeddings],
23285
+ ["maxWaitSeconds", maxWaitSeconds],
23286
+ ["priority", priority],
23287
+ ["timeoutSeconds", timeoutSeconds],
23288
+ ["stopOnRepetition", stopOnRepetition],
23289
+ ["defer", defer]
23290
+ ].filter(([, value]) => value !== void 0).map(([name]) => `\`${name}\``);
23291
+ if (executionOnlyFields.length > 0) {
23292
+ return errorResult(
23293
+ new Error(
23294
+ `\`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.`
23295
+ )
23296
+ );
23297
+ }
23298
+ }
23299
+ const effectiveRequiredCapabilities = tools === void 0 ? requiredCapabilities : withRequiredCapability(requiredCapabilities, "tools");
23300
+ if (preview) {
23301
+ return parsedResult(
23302
+ await route2(endpoint, canonicalTask, objective, model, sessionId, contextTokens, effectiveRequiredCapabilities, minConfidence, executionPreference, minPlacementEvidence)
23303
+ );
23304
+ }
23305
+ const problem = validateTaskPayload({
23306
+ task,
23307
+ scopeId,
23308
+ scopeRevision,
23309
+ prompt,
23310
+ systemPrompt,
23311
+ messages,
23312
+ images,
23313
+ input,
23314
+ tools,
23315
+ format,
23316
+ think,
23317
+ logprobs,
23318
+ topLogprobs,
23319
+ returnEmbeddings
23320
+ });
23321
+ if (problem) return errorResult(new Error(problem));
23322
+ if (minConfidence && defer && scopeId === void 0) {
23323
+ const decision = parsedResult(
23324
+ await route2(endpoint, canonicalTask, objective, model, sessionId, contextTokens, effectiveRequiredCapabilities, minConfidence, executionPreference, minPlacementEvidence)
23325
+ );
23326
+ if (!isStructuredSuccess(decision)) return decision;
23327
+ }
23328
+ const result = parsedResult(
23329
+ await runTaskRequest2(endpoint ?? DEFAULT_SERVE_ENDPOINT, {
23330
+ task: scopeId !== void 0 && task === void 0 ? void 0 : canonicalTask,
23331
+ objective: objective ?? (scopeId === void 0 ? "balanced" : void 0),
23332
+ model,
23333
+ session_id: sessionId,
23334
+ scope_id: scopeId,
23335
+ scope_revision: scopeRevision,
23336
+ context_tokens: contextTokens,
23337
+ required_capabilities: effectiveRequiredCapabilities,
23338
+ prompt,
23339
+ images,
23340
+ messages: taskMessages({ systemPrompt, messages, prompt, images }),
23341
+ input,
23342
+ tools,
23343
+ keep_alive: keepAlive,
23344
+ min_confidence: minConfidence,
23345
+ priority: priority ?? "normal",
23346
+ max_wait_seconds: maxWaitSeconds,
23347
+ timeout_seconds: timeoutSeconds,
23348
+ stop_on_repetition: stopOnRepetition,
23349
+ defer: defer ?? false,
23350
+ execution_preference: executionPreference,
23351
+ min_placement_evidence: minPlacementEvidence,
23352
+ request_options: {
23353
+ format,
23354
+ think,
23355
+ options,
23356
+ logprobs,
23357
+ top_logprobs: topLogprobs
23358
+ }
23359
+ })
23360
+ );
23361
+ if (isStructuredSuccess(result) && result.structuredContent.deferred === true) return result;
23362
+ if (!returnEmbeddings && isStructuredSuccess(result)) {
23363
+ const trimmed = summarizeEmbeddings(result.structuredContent);
23364
+ if (trimmed) return withTaskTelemetry(structuredResult(trimmed));
23365
+ }
23366
+ return withTaskTelemetry(result);
23367
+ } catch (error2) {
23368
+ return errorResult(error2);
23369
+ }
23370
+ }
23371
+ );
23372
+ }
23373
+
23374
+ // src/tools/task-jobs.ts
23375
+ function registerTaskJobsTool(server2) {
23376
+ server2.registerTool(
23377
+ "task_jobs",
23378
+ {
23379
+ 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.",
23380
+ inputSchema: external_exports.object({
23381
+ endpoint: endpointParam,
23382
+ action: external_exports.enum(["list", "get", "cancel", "remove"]),
23383
+ jobId: external_exports.string().uuid().optional(),
23384
+ returnEmbeddings: external_exports.boolean().optional()
23385
+ }).strict(),
23386
+ outputSchema: objectResultSchema,
23387
+ annotations: { destructiveHint: true }
23388
+ },
23389
+ async ({ endpoint, action, jobId, returnEmbeddings }) => {
23390
+ try {
23391
+ if (action === "list" ? jobId !== void 0 : jobId === void 0) {
23392
+ return errorResult(new Error("Use jobId only with get/cancel/remove; all require a jobId."));
23393
+ }
23394
+ if (returnEmbeddings !== void 0 && action !== "get") {
23395
+ return errorResult(new Error("returnEmbeddings is valid only with get."));
23396
+ }
23397
+ const result = parsedResult(action === "list" ? await listTaskJobs2(endpoint) : action === "get" ? await getTaskJob2(endpoint, jobId) : action === "cancel" ? await cancelTaskJob2(endpoint, jobId) : await removeTaskJob2(endpoint, jobId));
23398
+ if (!returnEmbeddings && isStructuredSuccess(result)) {
23399
+ const job = result.structuredContent.job;
23400
+ if (job?.result && typeof job.result === "object") {
23401
+ const trimmed = summarizeEmbeddings(job.result);
23402
+ if (trimmed) return structuredResult({ ...result.structuredContent, job: { ...job, result: trimmed } });
23403
+ }
23404
+ }
23405
+ return result;
23406
+ } catch (error2) {
23407
+ return errorResult(error2);
23408
+ }
23409
+ }
23410
+ );
23411
+ }
23412
+
23413
+ // src/tools/ollama-lifecycle.ts
23414
+ var manageResultSchema = external_exports.object({
23415
+ status: external_exports.string().optional(),
23416
+ progress: external_exports.unknown().optional()
23417
+ }).passthrough();
23418
+ var deleteResultSchema = external_exports.object({
23419
+ deleted: external_exports.string()
23420
+ }).passthrough();
23421
+ function summarizeOllamaPullStream(text) {
23422
+ const events = [];
23423
+ for (const line of text.split(/\r?\n/)) {
23424
+ const trimmed = line.trim();
23425
+ if (!trimmed) continue;
22816
23426
  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");
23427
+ const parsed = JSON.parse(trimmed);
23428
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
23429
+ events.push(parsed);
22820
23430
  }
22821
- offset = parsed.offset;
22822
23431
  } catch {
22823
- throw new Error("`cursor` is invalid or the model list changed; restart without cursor.");
22824
23432
  }
22825
23433
  }
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) {
23434
+ if (events.length === 0) {
23435
+ if (!text.trim()) return { status: "empty" };
22838
23436
  try {
22839
- child2.kill("SIGKILL");
23437
+ const parsed = JSON.parse(text);
23438
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
23439
+ return parsed;
23440
+ }
22840
23441
  } catch {
23442
+ return { status: "unparsed", raw: clipText(text, 500) };
22841
23443
  }
23444
+ return { status: "unparsed", raw: clipText(text, 500) };
22842
23445
  }
22843
- liveDelegates.clear();
23446
+ const last = events[events.length - 1] ?? {};
23447
+ const withBytes = [...events].reverse().find((event) => typeof event.total === "number" && event.total > 0);
23448
+ const completed = typeof withBytes?.completed === "number" ? withBytes.completed : null;
23449
+ const total = typeof withBytes?.total === "number" ? withBytes.total : null;
23450
+ const percent = completed != null && total != null && total > 0 ? Math.round(completed / total * 1e3) / 10 : null;
23451
+ const status2 = typeof last.error === "string" && last.error ? "error" : last.status;
23452
+ return {
23453
+ ...last,
23454
+ ...status2 === void 0 ? {} : { status: status2 },
23455
+ progress: {
23456
+ events: events.length,
23457
+ completed,
23458
+ total,
23459
+ percent,
23460
+ lastStatus: last.status ?? null
23461
+ }
23462
+ };
22844
23463
  }
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;
23464
+ async function ollamaPull(endpoint, model, timeoutSeconds, options = {}) {
23465
+ const raw = await ollamaFetch(endpoint, "/api/pull", {
23466
+ method: "POST",
23467
+ body: { model, stream: true },
23468
+ timeoutMs: (timeoutSeconds ?? DEFAULT_PULL_TIMEOUT_SECONDS) * 1e3,
23469
+ parse: "ndjson",
23470
+ signal: options.signal,
23471
+ onLine: options.onProgress
22849
23472
  });
23473
+ if (typeof raw === "string") return summarizeOllamaPullStream(raw);
23474
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) return raw;
23475
+ return { status: "empty" };
22850
23476
  }
22851
- process.on("exit", () => {
22852
- killLiveDelegates();
22853
- stopAutostartedServe();
22854
- });
22855
- process.stdin.on("close", () => {
22856
- killLiveDelegates();
22857
- stopAutostartedServe();
22858
- process.exit(0);
22859
- });
22860
23477
  function pullProgressReporter(extra) {
22861
23478
  const token = extra._meta?.progressToken;
22862
23479
  if (token === void 0) return void 0;
@@ -22880,769 +23497,245 @@ function pullProgressReporter(extra) {
22880
23497
  }).catch(() => void 0);
22881
23498
  };
22882
23499
  }
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,
23500
+ function registerOllamaLifecycleTools(server2) {
23501
+ server2.registerTool(
23502
+ "ollama_manage",
22923
23503
  {
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}.`
23504
+ 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.",
23505
+ inputSchema: external_exports.object({
23506
+ action: external_exports.enum(["pull", "stop"]),
23507
+ model: external_exports.string().min(1),
23508
+ ollamaEndpoint: ollamaEndpointParam,
23509
+ timeoutSeconds: external_exports.number().int().positive().optional()
23510
+ }).strict(),
23511
+ outputSchema: manageResultSchema,
23512
+ annotations: { destructiveHint: false }
22926
23513
  },
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;
23514
+ async ({ action, model, ollamaEndpoint, timeoutSeconds }, extra) => {
22952
23515
  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)}`;
23516
+ if (action === "stop" && timeoutSeconds !== void 0) {
23517
+ return errorResult(new Error('`timeoutSeconds` is valid only for action "pull".'));
23518
+ }
23519
+ const data = action === "pull" ? await ollamaPull(ollamaEndpoint, model, timeoutSeconds, {
23520
+ signal: extra.signal,
23521
+ onProgress: pullProgressReporter(extra)
23522
+ }) : await ollamaFetch(ollamaEndpoint, "/api/generate", {
23523
+ method: "POST",
23524
+ body: { model, keep_alive: 0 },
23525
+ signal: extra.signal
23526
+ });
23527
+ const payload = data;
23528
+ if (typeof payload.error === "string" && payload.error) {
23529
+ return errorResult(new Error(`${action} ${model} failed: ${payload.error}`));
22958
23530
  }
22959
- }
22960
- try {
22961
- report.structuredContent.platform_health = JSON.parse(await health(serveEndpoint));
23531
+ return structuredResult(payload);
22962
23532
  } catch (error2) {
22963
- report.structuredContent.platform_health_unavailable = `freellama serve unreachable: ${error2 instanceof Error ? error2.message : String(error2)}`;
23533
+ return errorResult(error2);
22964
23534
  }
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
23535
+ }
23536
+ );
23537
+ server2.registerTool(
23538
+ "ollama_delete",
23539
+ {
23540
+ 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.",
23541
+ inputSchema: external_exports.object({
23542
+ model: external_exports.string().min(1),
23543
+ ollamaEndpoint: ollamaEndpointParam
23544
+ }).strict(),
23545
+ outputSchema: deleteResultSchema,
23546
+ annotations: { destructiveHint: true }
23547
+ },
23548
+ async ({ model, ollamaEndpoint }) => {
23549
+ try {
23550
+ await ollamaFetch(ollamaEndpoint, "/api/delete", {
23551
+ method: "DELETE",
23552
+ body: { model }
22977
23553
  });
23554
+ return structuredResult({ deleted: model });
23555
+ } catch (error2) {
23556
+ return errorResult(error2);
22978
23557
  }
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
- }
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
23558
  }
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
- }
23559
+ );
23560
+ }
23561
+
23562
+ // src/tools/delegate-research.ts
23563
+ import { existsSync as existsSync5 } from "node:fs";
23564
+ import { mkdtemp, readFile as readFile2, rm, writeFile } from "node:fs/promises";
23565
+ import { tmpdir } from "node:os";
23566
+ import path5 from "node:path";
23567
+
23568
+ // src/delegate.ts
23569
+ import { existsSync as existsSync4, readFileSync, realpathSync, statSync } from "node:fs";
23570
+ import path4 from "node:path";
23571
+ function researchErrorResult(error2, diagnostic) {
23572
+ const result = errorResult(error2);
23573
+ const summary = diagnostic ?? result.content[0].text;
23574
+ return {
23575
+ ...result,
23576
+ content: [{ type: "text", text: summary }],
23577
+ structuredContent: {
23578
+ ...result.structuredContent ?? { error: summary },
23579
+ answer: "",
23580
+ summary,
23581
+ verification: {
23582
+ recommendation: "escalate",
23583
+ grounded: false,
23584
+ why: "The research request failed; no usable answer was produced."
23162
23585
  }
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
23586
  }
23192
- return { installed, memoryBytes };
23193
23587
  };
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
23588
  }
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
- }) => {
23589
+ var MODEL_EVIDENCE = (() => {
23590
+ const configured = process.env.FREELLAMA_MCP_MODEL_EVIDENCE;
23591
+ const candidates = [
23592
+ configured,
23593
+ path4.join(REPO_ROOT, "packages/mcp/evidence/model-evidence.json")
23594
+ ].filter((x) => Boolean(x));
23595
+ for (const file of candidates) {
23343
23596
  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);
23597
+ if (!existsSync4(file)) continue;
23598
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
23599
+ return parsed.models ?? {};
23467
23600
  } catch (error2) {
23468
- return errorResult(error2);
23601
+ process.stderr.write(
23602
+ `freellama-mcp: ignoring unreadable model evidence ${file}: ${error2 instanceof Error ? error2.message : String(error2)}
23603
+ `
23604
+ );
23469
23605
  }
23470
23606
  }
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
- }
23607
+ return {};
23608
+ })();
23609
+ var STRONG_MODEL_HINT = "Re-run with a model graded strong in packages/mcp/evidence/model-evidence.json";
23610
+ function assessDelegatedAnswer(question, evidenceCount, model) {
23611
+ const grounded = evidenceCount > 0;
23612
+ const evidence = MODEL_EVIDENCE[model];
23613
+ if (evidence?.grade === "unusable") {
23614
+ return {
23615
+ recommendation: "escalate",
23616
+ grounded,
23617
+ 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.`,
23618
+ measuredBaseRate: `${model}: ${evidence.note}`
23619
+ };
23505
23620
  }
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
- }
23621
+ if (!evidence) {
23622
+ return {
23623
+ recommendation: "verify",
23624
+ grounded,
23625
+ 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.`,
23626
+ measuredBaseRate: "no measured base rate for this model"
23627
+ };
23582
23628
  }
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
- }
23629
+ if (evidence.grade === "weak" && grounded) {
23630
+ return {
23631
+ recommendation: "verify",
23632
+ grounded,
23633
+ 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.`,
23634
+ measuredBaseRate: `${model}: ${evidence.note}`
23635
+ };
23618
23636
  }
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 }) => {
23637
+ if (!grounded) {
23638
+ return {
23639
+ recommendation: "escalate",
23640
+ grounded: false,
23641
+ 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.",
23642
+ measuredBaseRate: "ungrounded answers have no measured accuracy \u2014 they were never the tested path"
23643
+ };
23644
+ }
23645
+ 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;
23646
+ if (judgmentSignals.test(question)) {
23647
+ return {
23648
+ recommendation: "verify",
23649
+ grounded: true,
23650
+ 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.",
23651
+ measuredBaseRate: `${model}: ${evidence.note}; measured on lookups, not judgment calls`
23652
+ };
23653
+ }
23654
+ if (evidenceCount > 5) {
23655
+ return {
23656
+ recommendation: "verify",
23657
+ grounded: true,
23658
+ 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.`,
23659
+ measuredBaseRate: `${model}: ${evidence.note}; measured within a 1-5 file scope`
23660
+ };
23661
+ }
23662
+ return {
23663
+ recommendation: "accept",
23664
+ grounded: true,
23665
+ 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}).`,
23666
+ measuredBaseRate: `${model}: ${evidence.note}`
23667
+ };
23668
+ }
23669
+ function extractExistingWorkspacePath(command, workspace) {
23670
+ let root;
23671
+ try {
23672
+ root = realpathSync(workspace);
23673
+ } catch {
23674
+ return null;
23675
+ }
23676
+ const tokens = command.match(/(?:[^\s"'\\]+|"(?:\\.|[^"])*"|'[^']*')+/g) ?? [];
23677
+ const patternFlags = /* @__PURE__ */ new Set(["-name", "-iname", "-path", "-ipath", "-regex", "-iregex", "-wholename"]);
23678
+ for (let index = 0; index < tokens.length; index += 1) {
23679
+ const raw = tokens[index] ?? "";
23680
+ if (index > 0 && patternFlags.has(tokens[index - 1] ?? "")) continue;
23681
+ const token = raw.replace(/^["']|["']$/g, "").replace(/[|;&)]+$/g, "").replace(/:\d+(?::\d+)?$/, "");
23682
+ if (!token || token.startsWith("-") || /[*?$`]/.test(token)) continue;
23683
+ const candidate = path4.isAbsolute(token) ? token : path4.resolve(root, token);
23684
+ if (!existsSync4(candidate)) continue;
23632
23685
  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);
23686
+ const resolved = realpathSync(candidate);
23687
+ const relative = path4.relative(root, resolved);
23688
+ const withinRoot = relative && relative !== ".." && !relative.startsWith(`..${path4.sep}`);
23689
+ if (withinRoot && statSync(resolved).isFile()) return relative;
23690
+ } catch {
23640
23691
  }
23641
23692
  }
23642
- );
23643
- async function routedResearchModel(endpoint, executionPreference, minPlacementEvidence) {
23693
+ return null;
23694
+ }
23695
+ var adapterCallSchema = external_exports.object({
23696
+ raw_name: external_exports.string().optional(),
23697
+ status: external_exports.string().optional(),
23698
+ arguments: external_exports.object({
23699
+ tool: external_exports.string().optional(),
23700
+ command: external_exports.string().optional(),
23701
+ queries: external_exports.object({ path: external_exports.string().optional() }).passthrough().optional()
23702
+ }).passthrough().optional()
23703
+ }).passthrough();
23704
+ var adapterResultSchema = external_exports.object({
23705
+ final_answer: external_exports.string(),
23706
+ tool_calls: external_exports.array(adapterCallSchema).default([]),
23707
+ usage: external_exports.object({
23708
+ input_tokens: external_exports.number().nullable().optional(),
23709
+ output_tokens: external_exports.number().nullable().optional()
23710
+ }).passthrough().default({}),
23711
+ model_metadata: external_exports.object({
23712
+ context_compactions: external_exports.number().int().nonnegative().optional(),
23713
+ runtime_config: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
23714
+ context_management: external_exports.object({
23715
+ token_counting: external_exports.enum(["configured_estimate", "model_calibrated_estimate"]),
23716
+ estimate_scale: external_exports.number().positive(),
23717
+ calibration_samples: external_exports.number().int().nonnegative(),
23718
+ pinned_overflow: external_exports.enum(["error", "clip"]),
23719
+ compactions: external_exports.number().int().nonnegative()
23720
+ }).passthrough().optional()
23721
+ }).passthrough().optional()
23722
+ });
23723
+ function parseAdapterResult(raw) {
23724
+ return adapterResultSchema.parse(JSON.parse(raw));
23725
+ }
23726
+
23727
+ // src/tools/delegate-research.ts
23728
+ var researchResultSchema = external_exports.object({
23729
+ answer: external_exports.string(),
23730
+ summary: external_exports.string(),
23731
+ citations: external_exports.unknown().optional(),
23732
+ verification: external_exports.unknown(),
23733
+ usage: external_exports.unknown().optional(),
23734
+ telemetry: external_exports.unknown().optional()
23735
+ }).passthrough();
23736
+ async function routedResearchModel(endpoint) {
23644
23737
  const decision = JSON.parse(
23645
- await route2(endpoint, "coding", "balanced", void 0, void 0, void 0, [], void 0, executionPreference, minPlacementEvidence)
23738
+ await route2(endpoint, "coding", "balanced", void 0, void 0, void 0, [], void 0, void 0, void 0)
23646
23739
  );
23647
23740
  if (typeof decision.selected_model !== "string" || !decision.selected_model) {
23648
23741
  throw new Error(
@@ -23651,196 +23744,201 @@ async function routedResearchModel(endpoint, executionPreference, minPlacementEv
23651
23744
  }
23652
23745
  return decision.selected_model;
23653
23746
  }
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;
23747
+ function registerDelegateResearchTool(server2) {
23748
+ server2.registerTool(
23749
+ "delegate_research",
23750
+ {
23751
+ 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.",
23752
+ inputSchema: external_exports.object({
23753
+ question: external_exports.string().min(1),
23754
+ workspacePath: external_exports.string().min(1),
23755
+ adapter: external_exports.enum(["bash", "octocode"]).optional(),
23756
+ model: external_exports.string().min(1).optional(),
23757
+ endpoint: endpointParam,
23758
+ agent: external_exports.object({
23759
+ maxTurns: external_exports.number().int().positive().optional(),
23760
+ contextTokens: external_exports.number().int().positive().optional(),
23761
+ outputTokens: external_exports.number().int().positive().optional()
23762
+ }).strict().optional()
23763
+ }).strict(),
23764
+ outputSchema: researchResultSchema,
23765
+ annotations: { destructiveHint: false }
23766
+ },
23767
+ async ({ question, workspacePath, adapter, model, endpoint, agent }, extra) => {
23768
+ const chosenAdapter = adapter ?? DEFAULT_RESEARCH_ADAPTER;
23769
+ let resolvedWorkspace;
23770
+ let chosenModel;
23754
23771
  try {
23755
- await running;
23772
+ resolvedWorkspace = await assertAllowedWorkspace(workspacePath);
23773
+ chosenModel = model ?? DEFAULT_DELEGATE_MODEL ?? await routedResearchModel(endpoint);
23756
23774
  } catch (error2) {
23757
- adapterError = error2;
23758
- } finally {
23759
- liveDelegates.delete(running.child);
23775
+ return researchErrorResult(error2);
23760
23776
  }
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
- );
23777
+ const known = MODEL_EVIDENCE[chosenModel];
23778
+ if (known?.grade === "unusable") {
23779
+ return structuredResult({
23780
+ adapter: chosenAdapter,
23781
+ verification: assessDelegatedAnswer(question, 0, chosenModel),
23782
+ answer: "",
23783
+ toolCallCount: 0,
23784
+ usage: { inputTokens: null, outputTokens: null },
23785
+ telemetry: costTelemetry({ inputTokens: null, outputTokens: null }, EXTERNAL_COST),
23786
+ evidence: [],
23787
+ summary: `Refused before running: ${chosenModel} is measured unusable for research here (${known.note}). ${STRONG_MODEL_HINT}, or answer it yourself.`
23788
+ });
23770
23789
  }
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 ? `
23790
+ const dir = await mkdtemp(path5.join(tmpdir(), "freellama-delegate-"));
23791
+ const promptFile = path5.join(dir, "prompt.md");
23792
+ const resultFile = path5.join(dir, "result.json");
23793
+ try {
23794
+ await writeFile(promptFile, `${question}
23795
+ `, "utf8");
23796
+ const adapter2 = RESEARCH_ADAPTERS[chosenAdapter];
23797
+ if (!existsSync5(adapter2)) {
23798
+ return researchErrorResult(
23799
+ new Error(
23800
+ `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.`
23801
+ )
23802
+ );
23803
+ }
23804
+ const running = runDelegate(process.execPath, [adapter2], {
23805
+ signal: extra.signal,
23806
+ env: {
23807
+ ...delegateEnvironment(),
23808
+ FREELLAMA_TARGET_MODEL: chosenModel,
23809
+ FREELLAMA_AGENT_MANAGED_ENDPOINT: endpoint ?? DEFAULT_SERVE_ENDPOINT,
23810
+ FREELLAMA_AGENT_TOKEN_CALIBRATION_DIR: DEFAULT_TOKEN_CALIBRATION_DIR,
23811
+ FREELLAMA_BENCH_WORKSPACE: resolvedWorkspace,
23812
+ FREELLAMA_BENCH_PROMPT: promptFile,
23813
+ FREELLAMA_AGENT_RESULT: resultFile,
23814
+ FREELLAMA_AGENT_MAX_TURNS: String(agent?.maxTurns ?? DEFAULT_DELEGATE_MAX_TURNS),
23815
+ ...agent?.contextTokens !== void 0 ? { FREELLAMA_AGENT_NUM_CTX: String(agent.contextTokens) } : {},
23816
+ ...agent?.outputTokens !== void 0 ? { FREELLAMA_AGENT_NUM_PREDICT: String(agent.outputTokens) } : {}
23817
+ },
23818
+ timeoutMs: DEFAULT_DELEGATE_TIMEOUT_SECONDS * 1e3,
23819
+ // The answer is read from `resultFile`, never from stdout — but execFile's usual 1 MB
23820
+ // maxBuffer still applies to the agent's own progress logging, and overflowing it kills
23821
+ // the subprocess and surfaces as a research failure with no explanation. Give the logs
23822
+ // room; nothing here is proportional to the size of the answer.
23823
+ maxBuffer: 32 * 1024 * 1024
23824
+ // On timeout, abort, or overflow runDelegate SIGKILLs the whole process group: SIGTERM can
23825
+ // be swallowed by an adapter blocked in a long model call, and killing the adapter alone
23826
+ // would orphan `npx octocode` and sandbox shells.
23827
+ });
23828
+ liveDelegates.add(running.child);
23829
+ let adapterError = null;
23830
+ try {
23831
+ await running.done;
23832
+ } catch (error2) {
23833
+ adapterError = error2;
23834
+ } finally {
23835
+ liveDelegates.delete(running.child);
23836
+ killDelegateTree(running.child);
23837
+ }
23838
+ let result;
23839
+ try {
23840
+ result = parseAdapterResult(await readFile2(resultFile, "utf8"));
23841
+ } catch {
23842
+ return researchErrorResult(
23843
+ adapterError ?? new Error(
23844
+ `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.`
23845
+ )
23846
+ );
23847
+ }
23848
+ const evidence = result.tool_calls.map((call, index) => {
23849
+ const target = call.arguments?.queries?.path;
23850
+ const rawDetail = call.arguments?.command ?? null;
23851
+ const detail = rawDetail ? clipText(rawDetail, 400) : null;
23852
+ return {
23853
+ step: index + 1,
23854
+ tool: call.arguments?.tool ?? call.raw_name ?? "?",
23855
+ // The adapters record "ok" | "error" | "repeat" per call. Carrying it through is what
23856
+ // lets a reader tell a run that read three files from one that failed three commands —
23857
+ // indistinguishable in the trail before, and graded identically.
23858
+ status: call.status ?? "ok",
23859
+ path: target ? path5.relative(resolvedWorkspace, target) : detail ? extractExistingWorkspacePath(detail, resolvedWorkspace) : null,
23860
+ detail,
23861
+ detail_truncated: rawDetail !== detail
23862
+ };
23863
+ });
23864
+ const succeeded = evidence.filter((step) => step.status === "ok");
23865
+ const failed = evidence.length - succeeded.length;
23866
+ const evidenceText = evidence.map(
23867
+ (step) => ` ${step.step}. ${step.tool}${step.status === "ok" ? "" : ` [${step.status}]`}${step.path ? ` -> ${step.path}` : ""}${step.detail ? `: ${step.detail}` : ""}`
23868
+ ).join("\n");
23869
+ const citations = succeeded.map((step) => ({ step: step.step, tool: step.tool, path: step.path }));
23870
+ if (adapterError) {
23871
+ const diagnostic = `research adapter failed: ${result.final_answer}` + (evidenceText ? `
23795
23872
  Evidence collected before the failure:
23796
23873
  ${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)}
23874
+ const refusal = external_exports.object({ receipt: external_exports.object({ error: external_exports.string() }).passthrough() }).safeParse(result.model_metadata?.transport_error);
23875
+ if (refusal.success) {
23876
+ return researchErrorResult(new Error(JSON.stringify(refusal.data.receipt)), diagnostic);
23877
+ }
23878
+ return researchErrorResult(new Error(diagnostic));
23879
+ }
23880
+ const verification = assessDelegatedAnswer(question, succeeded.length, chosenModel);
23881
+ 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.`;
23882
+ const payload = {
23883
+ adapter: chosenAdapter,
23884
+ verification,
23885
+ answer: result.final_answer,
23886
+ // recommendation + why live under `verification`; `citations` completes the triple so a
23887
+ // caller never has to read `summary` to act on the result.
23888
+ citations,
23889
+ toolCallCount: result.tool_calls.length,
23890
+ successfulToolCallCount: succeeded.length,
23891
+ usage: {
23892
+ inputTokens: result.usage.input_tokens,
23893
+ outputTokens: result.usage.output_tokens
23894
+ },
23895
+ telemetry: costTelemetry({
23896
+ inputTokens: result.usage.input_tokens ?? null,
23897
+ outputTokens: result.usage.output_tokens ?? null
23898
+ }, EXTERNAL_COST),
23899
+ contextManagement: result.model_metadata?.context_management ?? null,
23900
+ execution: { receipts: result.model_metadata?.execution_receipts ?? [] },
23901
+ evidence,
23902
+ summary
23903
+ };
23904
+ const cited = [...new Set(citations.map((citation) => citation.path).filter(Boolean))];
23905
+ const answerText = `${clipText(result.final_answer, ANSWER_TEXT_MAX_CHARS)}
23833
23906
 
23834
23907
  verification: ${verification.recommendation}` + (cited.length ? `
23835
23908
  cited: ${cited.slice(0, 20).join(", ")}` : "") + `
23836
23909
  (${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 });
23910
+ return structuredResult(payload, { text: answerText });
23911
+ } catch (error2) {
23912
+ return researchErrorResult(error2);
23913
+ } finally {
23914
+ await rm(dir, { recursive: true, force: true });
23915
+ }
23842
23916
  }
23843
- }
23917
+ );
23918
+ }
23919
+
23920
+ // src/index.ts
23921
+ installProcessLifecycle();
23922
+ var INSTRUCTIONS = `models{view:"installed"}; models{view:"resident"}.
23923
+ 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.
23924
+ quality objective needs a task policy or explicit model; medium confidence needs policy+benchmark; observed placement needs /api/ps proof.
23925
+ Scoped run_task needs scopeId+scopeRevision. defer:true: job.id\u2192task_jobs.jobId unchanged. Restart loses scopes/sessions/jobs.
23926
+ Check isError first; prefer structuredContent, else content[].text. page.next_cursor\u2192cursor unchanged.
23927
+ 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.
23928
+ 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.
23929
+ serve :11435; default-endpoint autostart optional. Docs: freellama://docs/index.`;
23930
+ var server = new McpServer(
23931
+ { name: "freellama", version: SERVER_VERSION },
23932
+ { instructions: INSTRUCTIONS }
23844
23933
  );
23845
- var transport = new StdioServerTransport();
23934
+ registerDocsResources(server);
23935
+ registerDoctorTool(server);
23936
+ registerSessionTool(server);
23937
+ registerModelsTool(server);
23938
+ registerScopeTool(server);
23939
+ registerRunTaskTool(server);
23940
+ registerTaskJobsTool(server);
23941
+ registerOllamaLifecycleTools(server);
23942
+ registerDelegateResearchTool(server);
23943
+ var transport = withoutSchemaDialect(new StdioServerTransport());
23846
23944
  await server.connect(transport);