// src/profiles/researcher.ts
var DEFAULT_HARNESS = "opencode/zai-coding-plan/glm-5.1";
var DEFAULT_CITATION_DENSITY_MIN = 0.7;
function researcherProfile(options = {}) {
  const harness = options.harness ?? DEFAULT_HARNESS;
  const name = options.name ?? `researcher-${harness}`;
  const systemPrompt = options.systemPrompt ?? DEFAULT_RESEARCHER_SYSTEM_PROMPT;
  const citationDensityMin = options.citationDensityMin ?? DEFAULT_CITATION_DENSITY_MIN;
  const profile = {
    name,
    description: "Source-grounded research agent. Propose-don't-apply.",
    prompt: { systemPrompt },
    model: options.model ? { default: options.model } : void 0,
    tools: { web_search: true, fs: true, shell: true },
    metadata: { backendType: harness, role: "researcher" }
  };
  const output = { parse: parseResearcherEvents };
  const validator = options.task ? createResearcherValidator(options.task, { citationDensityMin }) : createResearcherValidator(
    { question: "", knowledgeNamespace: "" },
    { citationDensityMin, namespaceCheck: false }
  );
  const agentRunSpec = {
    name,
    profile,
    taskToPrompt: formatResearcherPrompt
  };
  return { profile, taskToPrompt: formatResearcherPrompt, output, validator, agentRunSpec };
}
function multiHarnessResearcherFanout(options = {}) {
  const harnesses = options.harnesses && options.harnesses.length > 0 ? options.harnesses : ["opencode/zai-coding-plan/glm-5.1", "claude-code", "codex"];
  const models = options.models ?? [];
  const agentRuns = harnesses.map((harness, i) => {
    const { agentRunSpec } = researcherProfile({ harness, model: models[i] });
    return agentRunSpec;
  });
  const { output, validator } = researcherProfile({
    citationDensityMin: options.citationDensityMin,
    task: options.task
  });
  const driver = {
    name: "researcher-fanout",
    async plan(task, history) {
      return history.length === 0 ? Array.from({ length: harnesses.length }, () => task) : [];
    },
    // 'done' is a terminal decision in the kernel; the loop finalizes after
    // the single fanout round and selects the winner via `defaultSelectWinner`.
    decide() {
      return "done";
    },
    describePlan() {
      return { kind: "fanout" };
    }
  };
  return { agentRuns, output, validator, driver };
}
function createResearcherValidator(task, config = {}) {
  const citationDensityMin = config.citationDensityMin ?? DEFAULT_CITATION_DENSITY_MIN;
  const namespaceCheck = config.namespaceCheck ?? true;
  return {
    async validate(output) {
      const notes = [];
      const scores = {};
      let pass = true;
      if (!Array.isArray(output.items) || output.items.length === 0) {
        pass = false;
        notes.push("no items");
        scores.items = 0;
      } else {
        scores.items = 1;
      }
      const missingEvidence = (output.items ?? []).filter(
        (item) => !Array.isArray(item.evidence) || item.evidence.length === 0
      );
      if (missingEvidence.length > 0) {
        pass = false;
        notes.push(`${missingEvidence.length} item(s) without evidence`);
        scores.provenance = 0;
      } else {
        scores.provenance = 1;
      }
      if (namespaceCheck) {
        const foreignItems = (output.items ?? []).filter(
          (item) => item.namespace !== task.knowledgeNamespace
        );
        const foreignWrites = (output.proposedWrites ?? []).filter(
          (write) => write.namespace !== task.knowledgeNamespace
        );
        if (foreignItems.length > 0 || foreignWrites.length > 0) {
          pass = false;
          notes.push(
            `namespace violation: ${foreignItems.length} item(s) + ${foreignWrites.length} write(s) outside ${task.knowledgeNamespace}`
          );
          scores.namespace = 0;
        } else {
          scores.namespace = 1;
        }
      }
      const itemCount = Math.max(output.items?.length ?? 0, 1);
      const citationsWithQuote = (output.citations ?? []).filter(
        (citation) => typeof citation.quote === "string" && citation.quote.length > 0
      ).length;
      const citationDensity = Math.min(1, citationsWithQuote / itemCount);
      if (citationDensity < citationDensityMin) {
        pass = false;
        notes.push(
          `citation density ${citationDensity.toFixed(2)} below floor ${citationDensityMin.toFixed(2)}`
        );
      }
      scores.citation_density = citationDensity;
      const sourceSet = /* @__PURE__ */ new Set();
      for (const item of output.items ?? []) {
        for (const evidence of item.evidence ?? []) {
          if (evidence.source) sourceSet.add(evidence.source);
        }
      }
      scores.source_diversity = Math.min(1, sourceSet.size / itemCount);
      scores.recency_match = recencyMatchScore(output.items ?? [], task.recencyWindow);
      const maxGaps = itemCount;
      const gapCount = output.gaps?.length ?? 0;
      scores.gap_coverage = Math.max(0, 1 - gapCount / maxGaps);
      const score = 0.4 * scores.citation_density + 0.2 * scores.source_diversity + 0.2 * scores.recency_match + 0.2 * scores.gap_coverage;
      const verdict = {
        valid: pass,
        score: Number.isFinite(score) ? score : 0,
        scores
      };
      if (notes.length > 0) verdict.notes = notes.join("; ");
      return verdict;
    }
  };
}
function recencyMatchScore(items, window) {
  if (!window || window.since === void 0 && window.until === void 0) return 1;
  if (items.length === 0) return 0;
  const sinceMs = window.since?.getTime() ?? Number.NEGATIVE_INFINITY;
  const untilMs = window.until?.getTime() ?? Number.POSITIVE_INFINITY;
  let hits = 0;
  let total = 0;
  for (const item of items) {
    for (const evidence of item.evidence ?? []) {
      if (typeof evidence.capturedAt !== "number") continue;
      total += 1;
      if (evidence.capturedAt >= sinceMs && evidence.capturedAt <= untilMs) hits += 1;
    }
  }
  return total === 0 ? 0 : hits / total;
}
var DEFAULT_RESEARCHER_SYSTEM_PROMPT = [
  "You are a research agent. Your job is to answer a research question with",
  "source-grounded knowledge items that the caller will choose whether to",
  "persist to a multi-tenant knowledge base.",
  "",
  "Hard rules:",
  "  1. Every item you emit MUST carry the task's knowledgeNamespace exactly.",
  "     Never write to a different namespace.",
  "  2. Every item MUST carry at least one evidence entry with a source.",
  "     A quote + url is strongly preferred; capturedAt is unix ms.",
  "  3. You propose writes \u2014 you do NOT apply them. The caller decides.",
  "  4. Self-report confidence honestly in [0, 1]. Do not inflate.",
  "  5. List what you could not answer in `gaps`. Better to admit a gap",
  "     than fabricate.",
  "",
  "When you finish, emit a single final structured message of the shape:",
  "  ```json",
  '  { "items": [{ "id": "...", "namespace": "...", "claim": "...",',
  '                "evidence": [{ "source": "...", "quote": "...",',
  '                               "url": "...", "capturedAt": 0 }],',
  '                "confidence": 0.0,',
  '                "authoredBy": { "kind": "agent", "id": "..." } }],',
  '    "citations": [{ "url": "...", "quote": "...", "confidence": 0.0 }],',
  '    "proposedWrites": [{ "kind": "insert", "namespace": "...",',
  '                         "item": { /* same shape as items[] */ } }],',
  '    "gaps": ["..."],',
  '    "notes": "free-form commentary" }',
  "  ```"
].join("\n");
function formatResearcherPrompt(task) {
  const sources = task.sources?.length ? task.sources.join(", ") : "(no preference)";
  const window = formatRecencyWindow(task.recencyWindow);
  return [
    `Question: ${task.question}`,
    `Knowledge namespace (DO NOT cross): ${task.knowledgeNamespace}`,
    `Scope: ${task.scope ?? "(unspecified)"}`,
    `Preferred sources: ${sources}`,
    `Recency window: ${window}`,
    `Max items: ${task.maxItems ?? "(no cap)"}`,
    `Per-item minimum confidence: ${task.minConfidence ?? "(no floor)"}`,
    "",
    "Produce knowledge items with provenance + citations + proposed writes.",
    "List gaps for anything you could not answer. Emit the final JSON",
    "result block exactly as instructed."
  ].join("\n");
}
function formatRecencyWindow(window) {
  if (!window) return "(none)";
  const since = window.since ? window.since.toISOString() : "-\u221E";
  const until = window.until ? window.until.toISOString() : "now";
  return `${since} .. ${until}`;
}
function parseResearcherEvents(events) {
  for (let i = events.length - 1; i >= 0; i -= 1) {
    const event = events[i];
    if (!event) continue;
    const type = String(event.type ?? "");
    const data = isRecord(event.data) ? event.data : {};
    if (type === "result" || type === "final" || type === "research.result") {
      const direct = coerceResearchOutput(data.result ?? data.output ?? data);
      const finalText = pickString(data.finalText);
      const fenced = finalText ? extractFencedJson(finalText) : void 0;
      const fromFinalText = fenced ? coerceResearchOutput(fenced) : void 0;
      if (direct && !isEmptyOutput(direct)) return direct;
      if (fromFinalText) return fromFinalText;
      if (direct) return direct;
    }
  }
  for (let i = events.length - 1; i >= 0; i -= 1) {
    const event = events[i];
    if (!event) continue;
    const data = isRecord(event.data) ? event.data : {};
    const text = pickString(data.text) ?? pickString(data.delta) ?? pickString(data.finalText);
    if (!text) continue;
    const fenced = extractFencedJson(text);
    if (!fenced) continue;
    const coerced = coerceResearchOutput(fenced);
    if (coerced) return coerced;
  }
  return { items: [], citations: [], proposedWrites: [] };
}
function isRecord(value) {
  return value !== null && typeof value === "object" && !Array.isArray(value);
}
function isEmptyOutput(o) {
  return o.items.length === 0 && o.citations.length === 0 && o.proposedWrites.length === 0;
}
function pickString(value) {
  return typeof value === "string" && value.length > 0 ? value : void 0;
}
function extractFencedJson(text) {
  const match = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
  if (!match) return void 0;
  const body = (match[1] ?? "").trim();
  if (!body) return void 0;
  try {
    return JSON.parse(body);
  } catch {
    return void 0;
  }
}
function coerceResearchOutput(value) {
  if (!isRecord(value)) return void 0;
  const items = coerceItems(value.items);
  const citations = coerceCitations(value.citations);
  const proposedWrites = coerceProposedWrites(value.proposedWrites);
  if (items === void 0 && citations === void 0 && proposedWrites === void 0) {
    return void 0;
  }
  const output = {
    items: items ?? [],
    citations: citations ?? [],
    proposedWrites: proposedWrites ?? []
  };
  if (Array.isArray(value.gaps)) {
    output.gaps = value.gaps.filter((entry) => typeof entry === "string");
  }
  const notes = pickString(value.notes);
  if (notes) output.notes = notes;
  const known = /* @__PURE__ */ new Set(["items", "citations", "proposedWrites", "gaps", "notes"]);
  const extras = {};
  let extrasCount = 0;
  for (const [key, val] of Object.entries(value)) {
    if (known.has(key)) continue;
    extras[key] = val;
    extrasCount += 1;
  }
  if (extrasCount > 0) output.raw = extras;
  return output;
}
function coerceItems(value) {
  if (!Array.isArray(value)) return void 0;
  const out = [];
  for (const entry of value) {
    if (!isRecord(entry)) continue;
    const id = pickString(entry.id);
    const namespace = pickString(entry.namespace);
    const claim = pickString(entry.claim);
    const evidence = coerceEvidence(entry.evidence);
    const confidence = toFiniteNumber(entry.confidence);
    const authoredBy = coerceAuthoredBy(entry.authoredBy);
    if (!id || !namespace || !claim || !authoredBy) continue;
    const item = {
      id,
      namespace,
      claim,
      evidence,
      confidence: clamp01(confidence),
      authoredBy
    };
    if (Array.isArray(entry.supersedes)) {
      item.supersedes = entry.supersedes.filter((s) => typeof s === "string");
    }
    const retractedAt = toFiniteNumber(entry.retractedAt);
    if (retractedAt > 0) item.retractedAt = retractedAt;
    out.push(item);
  }
  return out;
}
function coerceEvidence(value) {
  if (!Array.isArray(value)) return [];
  const out = [];
  for (const entry of value) {
    if (!isRecord(entry)) continue;
    const source = pickString(entry.source);
    if (!source) continue;
    const item = {
      source,
      capturedAt: toFiniteNumber(entry.capturedAt)
    };
    const quote = pickString(entry.quote);
    if (quote) item.quote = quote;
    const url = pickString(entry.url);
    if (url) item.url = url;
    out.push(item);
  }
  return out;
}
function coerceAuthoredBy(value) {
  if (!isRecord(value)) return void 0;
  const kind = value.kind === "human" || value.kind === "agent" ? value.kind : void 0;
  const id = pickString(value.id);
  if (!kind || !id) return void 0;
  return { kind, id };
}
function coerceCitations(value) {
  if (!Array.isArray(value)) return void 0;
  const out = [];
  for (const entry of value) {
    if (!isRecord(entry)) continue;
    const url = pickString(entry.url);
    const quote = pickString(entry.quote);
    if (!url || !quote) continue;
    out.push({ url, quote, confidence: clamp01(toFiniteNumber(entry.confidence)) });
  }
  return out;
}
function coerceProposedWrites(value) {
  if (!Array.isArray(value)) return void 0;
  const out = [];
  for (const entry of value) {
    if (!isRecord(entry)) continue;
    const namespace = pickString(entry.namespace);
    if (!namespace) continue;
    if (entry.kind === "insert") {
      const items = coerceItems([entry.item]);
      const item = items?.[0];
      if (!item) continue;
      out.push({ kind: "insert", namespace, item });
    } else if (entry.kind === "supersede") {
      const previousId = pickString(entry.previousId);
      const items = coerceItems([entry.item]);
      const item = items?.[0];
      if (!previousId || !item) continue;
      out.push({ kind: "supersede", namespace, previousId, item });
    } else if (entry.kind === "retract") {
      const itemId = pickString(entry.itemId);
      const reason = pickString(entry.reason);
      if (!itemId || !reason) continue;
      out.push({ kind: "retract", namespace, itemId, reason });
    }
  }
  return out;
}
function toFiniteNumber(value) {
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
}
function clamp01(value) {
  if (!Number.isFinite(value)) return 0;
  if (value < 0) return 0;
  if (value > 1) return 1;
  return value;
}

export {
  researcherProfile,
  multiHarnessResearcherFanout,
  createResearcherValidator
};
//# sourceMappingURL=chunk-5QOB7H74.js.map