import { ls, glob, grep, exec, ExecResult } from "std::shell"
import { Json } from "std::validation"

import {
  _compile,
  _compileFile,
  _typecheck,
  _typecheckFile,
  _getEffects,
  _describe,
  _parseAST,
  _writeAST,
  _format,
  _formatFile,
  _walkAST,
  _getNodesOfType,
  _filterImports,
  _subprocessDepth,
  _programTrustedModules,
  _exactVerdictFeedback,
  _failureFeedback,
  _readTestFileSandbox,
 } from "agency-lang/stdlib-lib/agency.js"
import { _prepareContainedPath } from "agency-lang/stdlib-lib/fs.js"
import {
  _loadTemplate,
  _holesOf,
  _fill,
  _parseExpr,
  _parseStatements,
  _toSource,
  _combine,
 } from "agency-lang/stdlib-lib/template.js"
import { _prepareFunctions } from "agency-lang/stdlib-lib/agencyFunctions.js"
import { _describeLearnedFunctions, _createLearnedTools } from "agency-lang/stdlib-lib/learnFunctions.js"
import { MAX_TOOL_NAME_LEN } from "std::skills"
import { invokeLearnedFunction } from "agency-lang/runtime"
import { VERSION } from "agency-lang/stdlib-lib/version.js"

/** @module
  @summary Compile, run, inspect, and generate Agency programs.
  Execute Agency source in a child process, inspect module exports and
  effects, or build source with AST and template helpers. Child operations
  still consult the parent's interrupt handlers.

  ```ts
  import { runCode } from "std::agency"

  node main() {
    const result = runCode("export node main() { return 42 }")
    print(result)
  }
  ```
*/

// `std::read`/`std::write` are the shared file-access effects (also raised by
// `std::read`/`std::write` in `std::index`), so their reads and writes here stay
// governed by the same file policy. The canonical payload is `{ dir, filename }`.
@alwaysUnder(dir)
effect std::read {
  dir: string;
  filename: string
}
@alwaysUnder(dir)
effect std::write {
  dir: string;
  filename: string
}
effect std::run {
  moduleId: string;
  node: string;
  args: Record<string, any>;
  limits: { wallClock: number; memory: number; ipcPayload: number; stdout: number; maxCost: number | null };
  cwd: string;
  logFile: string;
  depth: number;
  module: string;
  function: string;
  trustedModules: string[]
}

/** A compiled program returned by compile; retain the full returned value for execution. */
export type CompiledProgram = {
  moduleId: string
}

/** An installed standard-library module selected by its std:: import name. */
export type AgencyModuleSource = { kind: "module"; module: string }
/** An Agency file whose local imports remain inside dir. */
export type AgencyFileSource = { kind: "file"; dir: string; filename: string }
/** Source text or a complete Template Agency program, optionally with a local import directory. */
export type AgencyTextSource = { kind: "source"; source: string | Code; dir?: string }
export type AgencySource = AgencyModuleSource | AgencyFileSource | AgencyTextSource

/** Subprocess limits and working directory, with the same defaults as run. */
export type RunOptions = {
  wallClock?: number;
  memory?: number;
  ipcPayload?: number;
  stdout?: number;
  logFile?: string;
  cwd?: string;
  maxDepth?: number;
  maxCost?: number
}

type RunSettings = {
  wallClock: number;
  memory: number;
  ipcPayload: number;
  stdout: number;
  logFile: string;
  cwd: string;
  maxDepth: number;
  maxCost: number | null
}

// Every entry into _runProgram uses this one set of effective defaults.
def _runOptions(options: RunOptions): RunSettings {
  return {
    wallClock: options.wallClock ?? 60s,
    memory: options.memory ?? 512mb,
    ipcPayload: options.ipcPayload ?? 100mb,
    stdout: options.stdout ?? 1mb,
    logFile: options.logFile ?? "",
    cwd: options.cwd ?? "",
    maxDepth: options.maxDepth ?? 5,
    maxCost: options.maxCost ?? null
  }
}

export type SourceLocation = {
  line: number;
  col: number;
  start: number;
  end: number
}

export type TypeCheckDiagnostic = {
  /** Stable AG#### diagnostic code. Suppress one line with
  `// @tc-ignore AG####`, or match on it instead of parsing the message. */
  code: string;
  severity: string;
  message: string;
  loc?: SourceLocation;
  /** Structured payload of the diagnostic (the values rendered into the
  message, e.g. the expected and actual type strings; counts and positions
  are numbers). */
  params: Record<string, string | number>
}

export type TypeCheckReport = {
  errors: TypeCheckDiagnostic[];
  warnings: TypeCheckDiagnostic[]
}

/**
  Use `runCode` to execute source supplied by an LLM in one call. Use
  `compile` and `run` separately when Agency code can retain the full
  compiled value between calls. Compilation permits standard-library
  imports and local `.agency` files inside the given directory. It refuses
  JS/TS, Node and pkg:: imports, symlinks, and compile-time splices.
*/
@capabilities(fileRead)
export idempotent def compile(source: string, dir: string = "", strict: boolean = false): Result {
  """
  Compile Agency source for a later subprocess run. Only std:: and local
  .agency imports are supported; compile-time splices are refused.

  @param dir - Resolve and confine local imports within this directory, or leave empty to disallow them.
  @param strict - Also reject unresolved names and JavaScript globals outside the sandbox allowlist.
  """
  return try _compile(source, dir, strict)
}

/**
  Run a compiled program in a child process. Parent handlers and guards
  apply to the child, and supported lifecycle callbacks are forwarded.
  Streaming callbacks are not forwarded. A resource-limit failure reports
  `limit_exceeded`. Each execution segment gets its own wall-clock, memory,
  IPC, and output budget; paused time does not count. The lowest ancestor
  maxDepth remains in force.

  Success carries the subprocess result envelope, including its `data`
  value and usage information. Use `runCode` for just the node's value.

  The run interrupt's `trustedModules` lists the host's compiled files the
  program loads (see `sandbox.trustedModules` in agency.json); it is empty
  for a program that loads none.

  The run interrupt's module and function labels describe a prepared call;
  they do not verify compiled code stored in a checkpoint. Use
  module-specific approval rules only with trusted saved state. External
  hosts should verify checkpoint integrity before resuming saved code; see
  [checkpoint integrity](https://github.com/egonSchiele/agency-lang/blob/main/packages/agency-lang/docs/dev/runtime/checkpoint-integrity.md).
*/
@capabilities(fileWrite, subprocess)
export def run(
  compiled: CompiledProgram,
  node: string,
  args: Record<string, any> = {},
  wallClock?: number,
  memory?: number,
  ipcPayload?: number,
  stdout?: number,
  logFile?: string,
  cwd?: string,
  maxDepth?: number,
  maxCost?: number,
): Result {
  """
  Execute a compiled Agency program in a subprocess.

  @param compiled - The complete value returned by compile, including its captured code.
  @param node - The exported entry node to execute.
  @param args - Named arguments for the entry node.
  @param wallClock - The execution time limit in milliseconds, up to one hour, or null for the default.
  @param memory - The V8 heap limit in bytes, up to 4gb, or null for the default.
  @param ipcPayload - The limit per IPC message in bytes, up to 1gb, or null for the default.
  @param stdout - The combined stdout and stderr limit in bytes, up to 100mb, or null for the default.
  @param logFile - An optional statelog JSONL destination for this child run.
  @param cwd - The child working directory, or empty to inherit the caller's process directory.
  @param maxDepth - The subprocess nesting cap, up to ten, also bounded by ancestor caps.
  @param maxCost - The child's LLM spend limit in dollars, or null for no additional cost cap.
  """
  return _runProgram(compiled, node, args, {
    wallClock: wallClock, memory: memory, ipcPayload: ipcPayload, stdout: stdout,
    logFile: logFile, cwd: cwd, maxDepth: maxDepth, maxCost: maxCost
  })
}

// Only the prepared-function path supplies a source identity. An arbitrary
// program with a node called invokeLearned remains an ordinary run.
@capabilities(fileWrite, subprocess)
def _runProgram(
  compiled: CompiledProgram,
  node: string,
  args: Record<string, any>,
  options: RunOptions = {},
  sourceModule: string = "",
  functionName: string = "",
): Result {
  const settings = _runOptions(options)
  let configOverrides = null
  if (settings.logFile != "") {
    configOverrides = {
      observability: true,
      log: {
        logFile: settings.logFile
      }
    }
  }
  // Source labels are authority hints only for a trusted host program.
  // Sandboxed descendants can import hidden helpers, so their launches
  // must still pass the generic run gate, even with a supplied label.
  const originModule = if _subprocessDepth() == 0 then sourceModule else ""
  const originFunction = if originModule != "" then functionName else ""
  const message = if originModule != "" then "Running ${originModule} function ${originFunction} in subprocess" else "Running agent-generated code in subprocess"
  return interrupt std::run(message, {
    moduleId: compiled.moduleId,
    node: node,
    args: args,
    limits: {
      wallClock: settings.wallClock,
      memory: settings.memory,
      ipcPayload: settings.ipcPayload,
      stdout: settings.stdout,
      maxCost: settings.maxCost
    },
    cwd: settings.cwd,
    logFile: settings.logFile,
    depth: _subprocessDepth() + 1,
    module: originModule,
    function: originFunction,
    trustedModules: _programTrustedModules(compiled)
  })

  // guard() disables a dimension on a NEGATIVE value (null for BOTH
  // dimensions throws), so a null maxCost maps to the disable value and
  // one guarded call site serves both cases. Safe since #513: block
  // frames revive correctly across a nested subprocess pause.
  const NO_COST_CAP = -1.0
  const cap = if settings.maxCost != null then settings.maxCost else NO_COST_CAP
  const guarded = guard(cost: cap) {
    return try _run(
      compiled,
      node,
      args,
      settings.wallClock,
      settings.memory,
      settings.ipcPayload,
      settings.stdout,
      configOverrides,
      settings.cwd,
      settings.maxDepth,
    )
  }
  // guard() preserves the child Result. A guardFailure here belongs
  // to this call's cost cap, rather than the child result.
  if (guarded is failure(err)) {
    if (guarded.data.type == "guardFailure") {
      // Present cost caps in the same limit_exceeded shape as the other
      // run() limits (shape mirrors makeLimitFailure in lib/runtime/ipc.ts
      // — keep the two in sync).
      return failure(
        "Subprocess exceeded cost limit of ${settings.maxCost} (used ${guarded.data.actualCost})",
        {
          reason: "limit_exceeded",
          limit: "cost",
          threshold: settings.maxCost,
          value: guarded.data.actualCost
        },
      )
    }
  }
  return guarded
}

/**
  Call a public exported def without writing an entry node. The source
  can name a std:: module, a file inside a directory, or source text or
  complete Template Agency Code. Local imports must remain inside the
  directory; the same import restrictions as `compile` apply. Nodes and
  handoff functions are not supported.

  Each call starts a fresh subprocess with the parent handlers, policies,
  guards, and supported callbacks. A returned Result passes through
  unchanged; a plain value becomes a success.
*/
@capabilities(fileRead, fileWrite, subprocess)
export def runFunction(
  source: AgencySource,
  functionName: string,
  args: Record<string, any> = {},
  options: RunOptions = {},
): Result {
  """
  Call a public exported Agency function in a fresh subprocess.

  @param source - A std:: module, contained Agency file, or complete Agency source program.
  @param functionName - The public exported def to call, excluding nodes and handoff functions.
  @param args - Named arguments, with ordinary Agency defaults and aliases.
  @param options - Per-call resource limits, statelog destination, and working directory.
  """
  const prepared = try _prepareFunctions(source, [functionName])?
  return _executePrepared(prepared, functionName, args, options)
}

/** @hidden */
export def _executePrepared(
  prepared: any,
  exportName: string,
  args: Record<string, any> = {},
  options: RunOptions = {},
): Result {
  const outcome = _runProgram(
    prepared.compiled, prepared.invokeNode, { exportName: exportName, args: args },
    options, prepared.sourceModule, exportName,
  )?
  const value = outcome.data
  if (value is success) {
    return value
  }
  if (value is failure) {
    return value
  }
  return success(value)
}

/** @hidden */
export def _invokeLearned(target: any, args: Record<string, any>): any {
  return invokeLearnedFunction(target, args)
}

/**
  Creates callable tools with the source functions' descriptions, schemas,
  aliases, and argument binding. Import preparation captures local source
  files, so later calls use those bytes. A namespace prefixes advertised
  names as `namespace_exportName`.

  Learning runs module initialization in a subprocess to inspect tool
  metadata. That can pause, fail, or be rejected; it does not call the
  selected bodies. Inspection and each later tool call use the normal run
  gate and parent handlers. The module and function labels follow the
  trusted-state rules documented on `run`; the inspection launch uses the
  module label with `function` set to `<inspect>`, so a module-specific
  `std::run` rule can match or refuse it.

  The whole batch fails if a selected function is a handoff, needs a
  required function argument, or lacks a concrete parameter schema. The
  returned functions are not saved or added to an agent's tool list.
*/
@capabilities(fileRead, fileWrite, subprocess)
export def learn(
  source: AgencySource,
  names: string[] = [],
  namespace: string = "",
  options: RunOptions = {},
): Result<any[]> {
  """
  Create callable tools from public exported Agency functions. Module
  initialization runs during inspection and can pause or fail. The batch fails
  if any selected def is a handoff, requires a function argument, or lacks a
  concrete parameter schema.

  @param source - A std:: module, contained Agency file, or complete Agency source program.
  @param names - Public exported defs to select, or empty for all public defs.
  @param namespace - An optional prefix using letters, digits, underscores, or hyphens, keeping each full tool name within 64 characters.
  @param options - Resource limits, statelog destination, and working directory for inspection and calls.
  """
  const prepared = try _prepareFunctions(source, names, true)?
  const metadata = _runProgram(
    prepared.compiled, prepared.metadataNode, {}, options,
    prepared.sourceModule, "<inspect>",
  )?
  const definitions = metadata.data
  if (definitions is failure) {
    return definitions
  }
  return try _createLearnedTools(definitions, prepared, namespace, options, _executePrepared, MAX_TOOL_NAME_LEN)
}

/** @hidden */
export def _describeLearned(targets: any[], names: string[], callTargets: any[] = []): any {
  return _describeLearnedFunctions(targets, names, callTargets)
}

/**
  Compiles a file and runs its exported node through `run`, with the same
  parent handlers, guards, callbacks, and subprocess limits. Local imports
  remain within the supplied directory. The import restrictions on
  `compile` apply to the full dependency closure.
*/
@capabilities(fileRead)
export def runFile(
  dir: string,
  filename: string,
  node: string,
  args: Record<string, any> = {},
  wallClock: number = 60s,
  memory: number = 512mb,
  ipcPayload: number = 100mb,
  stdout: number = 1mb,
  maxCost: number | null = null,
): Result {
  """
  Compile an Agency file and execute its exported node in a subprocess.
  Only std:: and contained .agency imports are supported.

  @param dir - The directory containing the file and confining its local imports.
  @param filename - A relative Agency filename that stays inside dir.
  @param node - The exported entry node to execute.
  @param args - Named arguments for the entry node.
  @param wallClock - The execution time limit in milliseconds, up to one hour.
  @param memory - The V8 heap limit in bytes, up to 4gb.
  @param ipcPayload - The limit per IPC message in bytes, up to 1gb.
  @param stdout - The combined stdout and stderr limit in bytes, up to 100mb.
  @param maxCost - The child's LLM spend limit in dollars, or null for no additional cost cap.
  """
  const compileResult = try _compileFile(dir, filename)
  if (isFailure(compileResult)) {
    return compileResult
  }
  return run(
    compiled: compileResult.value,
    node: node,
    args: args,
    wallClock: wallClock,
    memory: memory,
    ipcPayload: ipcPayload,
    stdout: stdout,
    maxCost: maxCost,
  )
}

export type InterruptAnswer = {
  action: "approve" | "reject";
  value?: any;
  expectedMessage?: string
}

export type TestArguments = Record<string, any>

export type AgencyTestCase = {
  node: string;
  args?: TestArguments;
  expected: any;
  interrupts?: InterruptAnswer[];
  wallClock?: number;
  description?: string
}

export type CaseReport = {
  node: string;
  pass: boolean;
  feedback: string
}

export type TestReport = {
  pass: boolean;
  cases: CaseReport[]
}

/**
  Each case runs in its own child process through `run`. Parent handlers
  vote on both the launch and the tested code's interrupts. Scripted
  interrupt answers add handler votes in order; a parent rejection still
  wins. Exhausted answers let outer handlers decide. Leftover answers or
  an expected-message mismatch fail the case.

  Compilation errors fail the whole call. A failed value check, rejected
  interrupt, or per-case resource limit appears in the report and does
  not stop later cases. The cost cap covers the whole batch and returns a
  `limit_exceeded` failure when reached.
*/
@capabilities(fileRead)
export def test(
  dir: string,
  filename: string,
  cases: AgencyTestCase[],
  wallClock: number = 60s,
  memory: number = 512mb,
  ipcPayload: number = 100mb,
  stdout: number = 1mb,
  maxCost: number | null = null,
): Result<TestReport> {
  """
  Run test cases against an Agency file in subprocesses and report each
  case's result. Only std:: and contained .agency imports are supported.

  @param dir - The directory containing the tested file and confining local imports.
  @param filename - A relative Agency source filename that stays inside dir.
  @param cases - Exported entry nodes, named arguments, expected values, and optional scripted interrupt answers.
  @param wallClock - Milliseconds per case, overridden by a case's own wallClock value.
  @param memory - The V8 heap limit per case in bytes.
  @param ipcPayload - The byte limit per IPC message in each case.
  @param stdout - The combined stdout and stderr limit per case in bytes.
  @param maxCost - The LLM spend limit in dollars for the whole batch, or null for no additional cost cap.
  """
  const compiled = try _compileFile(dir, filename)
  if (compiled is failure(compileErr)) {
    return compiled
  }
  let program = null
  if (compiled is success(value)) {
    program = value
  }

  // Same guard pattern as run(): a negative cap disables the dimension, so
  // one guarded call site serves both cases.
  const NO_COST_CAP = -1.0
  const cap = if maxCost != null then maxCost else NO_COST_CAP
  const guarded = guard(cost: cap) {
    const caseReports: CaseReport[] = []
    let allPass = true
    for (testCase in cases) {
      // The scripted-answer state the inline handler mutates: which answer
      // is next, and whether an expectedMessage mismatch already decided
      // this case.
      const scripted = {
        index: 0,
        mismatch: ""
      }
      const answers = testCase.interrupts ?? []
      let runResult = null
      handle {
        runResult = run(
          compiled: program,
          node: testCase.node,
          args: testCase.args ?? {},
          wallClock: testCase.wallClock ?? wallClock,
          memory: memory,
          ipcPayload: ipcPayload,
          stdout: stdout,
        )
      } with (intr) {
        // The case's own sandbox launch: approve. The parent still sees it
        // and any parent reject wins. A std::run for a different module
        // (tested code launching its own subprocess) falls through to the
        // scripted answers like any other effect.
        if (intr.effect == "std::run") {
          if (intr.data.moduleId == program.moduleId) {
            return approve()
          }
        }
        if (scripted.index < answers.length) {
          const answer = answers[scripted.index]
          scripted.index = scripted.index + 1
          // Presence decides whether the message is checked (== null also
          // matches undefined); an empty string is a real expectation.
          if (answer.expectedMessage != null) {
            if (answer.expectedMessage != intr.message) {
              scripted.mismatch = "expected interrupt message \"${answer.expectedMessage}\", got \"${intr.message}\""
              return reject(scripted.mismatch)
            }
          }
          if (answer.action == "approve") {
            return approve(answer.value)
          }
          return reject("scripted answer rejected effect \"${intr.effect}\"")
        }
        // Answers exhausted: stay silent, so the interrupt propagates to
        // parent handlers (and the user) per the normal rules. Nothing is
        // approved by omission.
      }

      let feedback = ""
      if (scripted.mismatch != "") {
        feedback = scripted.mismatch
      } else if (scripted.index < answers.length) {
        feedback = "expected ${answers.length} interrupt(s), saw ${scripted.index}"
      } else if (runResult is failure(runErr)) {
        feedback = "run failed: ${_failureFeedback(runErr, runResult.data)}"
      } else if (runResult is success(envelope)) {
        // The node may return a failure. Unwrap it before the TypeScript
        // verdict helper, which cannot receive tagged failures.
        if (envelope.data is failure(nodeErr)) {
          feedback = "the node returned a failure: ${_failureFeedback(nodeErr, envelope.data.data)}"
        } else {
          feedback = _exactVerdictFeedback(envelope.data, testCase.expected)
        }
      }

      const casePassed = feedback == ""
      if (!casePassed) {
        allPass = false
      }
      caseReports.push(
        {
          node: testCase.node,
          pass: casePassed,
          feedback: feedback
        },
      )
    }
    return success({
      pass: allPass,
      cases: caseReports
    })
  }
  if (guarded is failure(err)) {
    if (guarded.data.type == "guardFailure") {
      // Same limit_exceeded shape as run()'s other limits (mirrors
      // makeLimitFailure in lib/runtime/ipc.ts — keep the two in sync).
      return failure(
        "Test run exceeded cost limit of ${maxCost} (used ${guarded.data.actualCost})",
        {
          reason: "limit_exceeded",
          limit: "cost",
          threshold: maxCost,
          value: guarded.data.actualCost
        },
      )
    }
  }
  return guarded
}

@capabilities(fileRead)
export def testFile(
  dir: string,
  filename: string,
  maxCost: number | null = null,
): Result<TestReport> {
  """
  Run the test cases declared in an Agency .test.json file.

  @param dir - The directory containing the test file and its source file.
  @param filename - A relative .test.json filename that stays inside dir.
  @param maxCost - The LLM spend limit in dollars for the whole batch, or null for no additional cost cap.
  """
  // Approve the file read before passing its prepared path to TypeScript.
  // The whole file is validated and converted up front, so a malformed file
  // is a whole-call failure with zero launches.
  const prepared = try _prepareContainedPath(dir, filename, "read")
  if (isFailure(prepared)) {
    return prepared
  }
  return interrupt std::read("Read this test file?", {
    dir: prepared.value.dir,
    filename: prepared.value.filename
  })
  const parsed = try _readTestFileSandbox(prepared.value.dir, prepared.value.filename)
  if (parsed is failure) {
    return parsed
  }
  if (parsed is success(wire)) {
    const cases: AgencyTestCase[] = []
    for (c in wire.cases) {
      cases.push(
        {
          node: c.node,
          args: c.args,
          expected: c.expected,
          interrupts: c.interrupts,
          wallClock: c.wallClock
        },
      )
    }
    return test(
      dir,
      wire.sourceFile,
      cases,
      wallClock: wire.defaultTimeoutMs ?? 60s,
      maxCost: maxCost,
    )
  }
  return failure("unreachable")
}

// Reject out-of-range limit values with a message the model can act on.
// Ceilings mirror LIMIT_CEILINGS in lib/runtime/ipc.ts (keep in sync).
def checkRunCodeLimits(
  wallClock: number,
  memory: number,
  ipcPayload: number,
  stdout: number,
): string {
  const checks = [
    {
      name: "wallClock",
      value: wallClock,
      max: 3600s,
      unit: "milliseconds"
    },
    {
      name: "memory",
      value: memory,
      max: 4096mb,
      unit: "bytes"
    },
    {
      name: "ipcPayload",
      value: ipcPayload,
      max: 1024mb,
      unit: "bytes"
    },
    {
      name: "stdout",
      value: stdout,
      max: 100mb,
      unit: "bytes"
    },
  ]
  for (check in checks) {
    if (check.value <= 0) {
      return "${check.name} must be a positive number of ${check.unit} (got ${check.value})."
    }
    if (check.value > check.max) {
      return "${check.name} is too large: ${check.value} ${check.unit} exceeds the maximum of ${check.max} ${check.unit}."
    }
  }
  return ""
}

/**
  Use this when an LLM supplies source code to execute. It combines
  compilation and execution so the model does not need to copy a compiled
  program value into another call. The node's value is returned without
  the subprocess envelope.

  Parent handlers, guards, and supported callbacks apply as in `run`.
  A compile error returns a failure before execution. The full import
  closure follows `compile` restrictions. `cwd` controls runtime file
  operations; `dir` separately controls local import resolution.

  The function is unmarked because arbitrary supplied code determines
  whether retrying is appropriate. Each new attempt raises the run gate
  and the child's own effects again.
*/
@capabilities(fileRead)
export def runCode(
  source: string,
  node: string = "main",
  args: Record<string, any> = {},
  wallClock: number = 60s,
  memory: number = 512mb,
  ipcPayload: number = 100mb,
  stdout: number = 1mb,
  maxCost: number | null = null,
  cwd: string = "",
  dir: string = "",
): Result {
  """
  Compile Agency source and execute an exported node in one call. Prefer
  this over separate compile and run calls. Only std:: and contained .agency
  imports are supported; splices are refused. Compile errors come back as a
  failure without running anything; fix the source and call again.

  @param node - The exported entry node to execute.
  @param args - Named arguments for the entry node.
  @param wallClock - A positive execution time limit in milliseconds, up to one hour.
  @param memory - A positive V8 heap limit in bytes, up to 4gb.
  @param ipcPayload - A positive byte limit per IPC message, up to 1gb.
  @param stdout - A positive combined stdout and stderr byte limit, up to 100mb.
  @param maxCost - The child's LLM spend limit in dollars, or null for no additional cost cap.
  @param cwd - The runtime working directory. Empty inherits the caller's process directory, which may not be where you want files; pass the agent working directory so the program's file writes land there.
  @param dir - Resolve and confine local imports within this directory, or leave empty to disallow them.
  """
  const limitError = checkRunCodeLimits(wallClock, memory, ipcPayload, stdout)
  if (limitError != "") {
    return failure(limitError)
  }
  const compileResult = try _compile(source, dir)
  if (compileResult is failure(compileErr)) {
    return compileResult
  }
  const result = run(
    compiled: compileResult.value,
    node: node,
    args: args,
    wallClock: wallClock,
    memory: memory,
    ipcPayload: ipcPayload,
    stdout: stdout,
    maxCost: maxCost,
    cwd: cwd,
  )
  // Remove the subprocess envelope so this tool returns the node's value.
  if (result is success(envelope)) {
    return success(envelope.data)
  }
  return result
}

/**
  Checks source text without writing it. With a directory, relative
  imports resolve as if the source were a file there. Use `typecheckFile`
  for an existing file.

  A normal check can accept imports that subprocess compilation refuses.
  Enable strict checking to apply the sandbox import and bound-name checks
  before trying to run generated code.
*/
@capabilities(fileRead)
export idempotent def typecheck(
  source: string,
  ignoreCodes: string[] = [],
  dir: string = "",
  strict: boolean = false,
): Result<TypeCheckReport> {
  """
  Type-check Agency source and return errors and warnings. To check a file
  already on disk, call typecheckFile with its path instead of pasting its
  contents. A clean report does not mean compile or run will accept the code
  unless strict is true.

  @param ignoreCodes - Diagnostic codes, such as AG3009, to omit from errors and warnings.
  @param dir - Resolve relative imports within this directory, or leave empty when no local imports are needed.
  @param strict - Also report sandbox import violations, unresolved names, and disallowed JavaScript globals.
  """
  const report = try _typecheck(source, dir, strict)
  if (report is failure) {
    return report
  }
  if (ignoreCodes.length === 0) {
    return report
  }
  const errors: TypeCheckDiagnostic[] = []
  for (diag in report.value.errors) {
    if (!ignoreCodes.includes(diag.code)) {
      errors.push(diag)
    }
  }
  const warnings: TypeCheckDiagnostic[] = []
  for (diag in report.value.warnings) {
    if (!ignoreCodes.includes(diag.code)) {
      warnings.push(diag)
    }
  }
  return success({
    errors: errors,
    warnings: warnings
  })
}

/** Per-exported-symbol effect lists, keyed by node/function name. */
export type EffectsByExport = Record<string, string[]>

/**
  Includes effects propagated through statically known calls. Bare
  interrupts appear as `unknown`. Compile-time generated code, functions
  received as parameters, and function references passed through variables
  can escape the analysis. Use this to inspect a program, with those limits
  in mind.
*/
@capabilities(fileRead)
export idempotent def getEffects(source: string): Result<EffectsByExport> {
  """
  List the interrupt effects each exported function or node can raise.
  Dynamic calls can escape the analysis, so an empty list does not prove
  that execution has no effects.
  """
  return try _getEffects(source)
}

/** One parameter of an exported def or node. */
export type ExportParam = {
  name: string;
  /** The declared type, printed the way the signature prints it. Null when none is declared. */
  type: string | null;
  /** The default as Agency source, such as `0` or `null`. Null when the parameter has none. */
  default: string | null;
  /** The text of the docstring's `@param` entry for this parameter, or null. */
  description: string | null
}

/** Metadata for one public export. The signature omits export and tool
  markers. For functions and nodes, docstring holds the tool description;
  for types, it holds the doc comment.

  Effects use the names and unknown marker returned by getEffects. Types
  and constants have no effects. A re-export names the outermost module
  it came through; an unresolved re-export has unknown effects. */
export type ExportInfo = {
  name: string;
  kind: "def" | "node" | "type" | "const" | "reexport";
  signature: string;
  /** The declared return type, printed on its own. Null when none is declared. */
  returnType: string | null;
  /** True when a def or node returns a `Result`, with the module's own aliases resolved. */
  returnsResult: boolean;
  docstring: string | null;
  effects: string[];
  destructive: boolean;
  idempotent: boolean;
  handoff: boolean;
  reexportedFrom: string | null;
  /** Each parameter of a def or node; empty for every other kind, and for a re-export that cannot be read from a source string. */
  params: ExportParam[]
}

export type ModuleInfo = {
  description: string | null;
  exports: ExportInfo[]
}

/**
  Use export metadata when generating code from a module, for example
  one handler per effect its tools can raise. It uses the visibility
  rules of `agency doc`: hidden and underscore-prefixed exports are
  omitted. The description is the module's one-line summary.

  For a module with local imports, supply its directory and filename along
  with the file's exact source text. The import closure is checked under
  the same restrictions as compilation. Imported source is read to compute
  metadata without a separate read interrupt. Relative re-exports remain
  entries with `unknown` effects; std:: re-exports resolve to full entries.
*/
@capabilities(fileRead)
export idempotent def describe(source: string, dir: string = "", filename: string = ""): Result<ModuleInfo> {
  """
  Describe an Agency module's public exports, signatures, documentation,
  and statically known effects.

  @param source - Module source text matching the file exactly when a filename is supplied.
  @param dir - Supply with filename to resolve local imports within this directory.
  @param filename - The module's path relative to dir, or empty when describing source without local imports.
  """
  return try _describe(source, dir, filename)
}

type FilterImportsResult = {
  source: string;
  filtered: boolean
}

/**
  Resolves imports relative to the file's own directory. This check does
  not apply subprocess import restrictions, so a clean report can still be
  refused by `runFile`.
*/
@capabilities(fileRead)
export idempotent def typecheckFile(dir: string, filename: string): Result raises <std::read> {
  """
  Type-check an Agency file on disk.

  @param filename - A relative Agency filename that stays inside dir.
  """
  const prepared = try _prepareContainedPath(dir, filename, "read")
  if (isFailure(prepared)) {
    return prepared
  }
  return interrupt std::read("Are you sure you want to read this file?", {
    dir: prepared.value.dir,
    filename: prepared.value.filename
  })
  return try _typecheckFile(prepared.value.dir, prepared.value.filename)
}

/** A parsed Agency program, the value `parseAST` returns on success.
  `type` is always "agencyProgram". `nodes` holds the top-level
  declarations (imports, functions, graph nodes, type aliases, ...). Each
  is an object with a `type` discriminant field. Its remaining fields
  vary by node type, so nodes stay untyped. */
export type AST = {
  type: "agencyProgram";
  nodes: any[];
  docComment?: any
}

export idempotent def parseAST(source: string): Result<AST> {
  """
  Parse Agency source into an abstract syntax tree.
  """
  return try _parseAST(source)
}

/**
  Writes the canonical formatter output, preserving comments while
  normalizing whitespace. The target must stay inside the supplied
  directory; symlinks below that directory are refused.
*/
@capabilities(fileRead, fileWrite)
export def writeAST(
  ast: AST,
  dir: string,
  filename: string,
  overwrite: boolean = true,
): Result raises <std::write> {
  """
  Format an Agency AST as source and write it to a file.

  @param filename - A relative output filename that stays inside dir.
  @param overwrite - If false, fail when the output file already exists.
  """
  const prepared = try _prepareContainedPath(dir, filename, "write")
  if (isFailure(prepared)) {
    return prepared
  }
  return interrupt std::write("Are you sure you want to write this file?", {
    dir: prepared.value.dir,
    filename: prepared.value.filename,
    overwrite: overwrite
  })
  return try _writeAST(ast, prepared.value.dir, prepared.value.filename, overwrite)
}

export idempotent def format(source: string): Result {
  """
  Format Agency source with the standard Agency formatter.
  """
  return try _format(source)
}

/** `Code` is `AST` plus a fragment kind: a value can hold a whole program
  (what `loadTemplate` and `parseAST` produce), a statement list, or a
  single expression. The kind is what lets an expression-sized fragment
  fill an expression hole. */
export type Code = {
  type: "agencyProgram";
  kind?: "program" | "statements" | "expr";
  nodes: any[];
  docComment?: any
}

export type HoleInfo = {
  name: string;
  sort: "expr" | "statements" | "identifier" | "decl";
  splice: boolean;
  type: string | null;
  origin: string | null
}

@capabilities(fileRead)
export def loadTemplate(dir: string, filename: string): Result<Code> raises <std::read> {
  """
  Read an Agency file containing template holes.

  @param filename - A relative template filename that stays inside dir.
  """
  const prepared = try _prepareContainedPath(dir, filename, "read")
  if (isFailure(prepared)) {
    return prepared
  }
  return interrupt std::read("Can I read this template?", {
    dir: prepared.value.dir,
    filename: prepared.value.filename
  })
  return try _loadTemplate(prepared.value.dir, prepared.value.filename)
}

/**
  Inspect remaining holes before filling a template. Each name appears
  once, in encounter order. The origin identifies the fill through which
  a grafted hole arrived when known; original holes have no origin.
*/
export idempotent def holesOf(template: Code): HoleInfo[] {
  """
  List the unfilled holes in an Agency code template.
  """
  return _holesOf(template)
}

/**
  Plain values become literal nodes; Code fragments are inserted as
  trees. String values are not parsed as source. A partial fill returns
  a template whose remaining holes can be filled later.
*/
export idempotent def fill(
  template: Code,
  values: Record<string, Json | Code>,
): Result<Code> {
  """
  Fill selected holes in an Agency code template.

  @param values - Map existing hole names to literal data or Code fragments of the required kind.
  """
  return try _fill(template, values)
}

/**
  Use this when generating one fragment per item, then joining the
  results. Fragments of the same kind retain that kind; expressions
  combine into a statement list. Whole programs cannot combine with
  loose expressions or statements.
*/
export idempotent def combine(codes: Code[]): Result<Code> {
  """
  Combine Agency code fragments in order. Whole programs cannot be mixed
  with loose statements or expressions.
  """
  return try _combine(codes)
}

export idempotent def toSource(code: Code): string {
  """
  Print Agency Code as source, preserving any unfilled template holes.
  """
  return _toSource(code)
}

export idempotent def parseExpr(source: string): Result<Code> {
  """
  Parse exactly one Agency expression into a Code fragment for an expression hole.
  """
  return try _parseExpr(source)
}

export idempotent def parseStatements(source: string): Result<Code> {
  """
  Parse Agency statements into a Code fragment for a statements hole.
  """
  return try _parseStatements(source)
}

/** Read and write happen inside the same interrupt, so approving it approves both.
  If the file is already formatted, no write occurs and its mtime is preserved. */
@capabilities(fileRead, fileWrite)
export idempotent def formatFile(dir: string, filename: string): Result raises <std::write> {
  """
  Format an Agency file in place.

  @param filename - A relative Agency filename that stays inside dir.
  """
  const prepared = try _prepareContainedPath(dir, filename, "write")
  if (isFailure(prepared)) {
    return prepared
  }
  return interrupt std::write("Are you sure you want to format this file in place?", {
    dir: prepared.value.dir,
    filename: prepared.value.filename
  })
  return try _formatFile(prepared.value.dir, prepared.value.filename)
}

/**
  Visits parents before children in a deep-cloned tree. Mutate each
  visited node in place to transform the returned clone. The original
  tree is unchanged.

  The visit list is fixed before callbacks run: added nodes are not
  visited, and replacing a child reference does not remove the old subtree
  from that list. Ancestors run from the root to the parent, excluding
  the visited node.
*/
export def walkAST(
  ast: AST,
  visitor: (node: any, ancestors: any[]) -> any,
): AST {
  """
  Visit and modify nodes in a cloned Agency AST.

  @param visitor - Called with the node and its ancestors, allowing in-place edits and ignoring the return value.
  """
  const result = _walkAST(ast)
  for (visit in result.visits) {
    visitor(visit.node, visit.ancestors)
  }
  return result.clone
}

export idempotent def getNodesOfType(
  source: string,
  types: string[],
): Result<any[]> {
  """
  Parse Agency source and find AST nodes matching the requested kinds.

  @param types - AST discriminants such as "function", "graphNode", or "importStatement".
  """
  return try _getNodesOfType(source, types)
}

export idempotent def getImports(source: string): Result<any[]> {
  """
  Find import statements in Agency source.
  """
  return getNodesOfType(source, ["importStatement"])
}

export idempotent def getFunctions(source: string): Result<any[]> {
  """
  Find function definitions in Agency source.
  """
  return getNodesOfType(source, ["function"])
}

export idempotent def getGraphNodes(source: string): Result<any[]> {
  """
  Find graph node definitions in Agency source.
  """
  return getNodesOfType(source, ["graphNode"])
}

/**
  Use to remove unwanted imports from generated source before further
  checking. This only filters import statements; it does not validate the
  remaining dependency closure. The returned source is formatted.

  Import kinds are stdlib for std:: paths, pkg for pkg:: paths, local for
  file paths, and node for bare Node specifiers. Package patterns use
  picomatch syntax against the original import path. Excludes always win.
  With no allow rules, remaining imports pass. Otherwise an allowed kind
  or an allowed path pattern is enough to retain an import.
*/
export idempotent def filterImports(
  source: string,
  allowedPackages: string[] = [],
  excludedPackages: string[] = [],
  allowKinds: string[] = [],
  excludeKinds: string[] = [],
): Result<{ source: string; filtered: boolean }> {
  """
  Remove imports that fail the supplied allow and exclude rules. Excludes
  win; empty rule lists permit every import.

  @param allowedPackages - Import-path globs to retain, unless an exclude rule matches.
  @param excludedPackages - Import-path globs to remove even when allowed elsewhere.
  @param allowKinds - Retain imports of kind stdlib, pkg, local, or node.
  @param excludeKinds - Remove imports of these kinds even when allowed elsewhere.
  """
  return try _filterImports(
    source,
    allowedPackages,
    excludedPackages,
    allowKinds,
    excludeKinds,
  )
}

export idempotent def getVersion(): string {
  """
  Get the current version of the Agency standard library.
  """
  return VERSION
}

/** Guidance for environments where the Agency CLI is not installed. */
export static const CLI_NOT_AVAILABLE = "The agency command line is not available here. Use the tools instead: typecheck(source) or typecheckFile(dir, filename) for type errors; testFile(dir, filename) to run a .test.json harness (write the files it tests first); runFile(dir, filename, node) to run a program; parseAST, format, describe for source analysis."

static const CLI_SUBCOMMANDS = ["test", "typecheck", "tc", "ast", "fmt", "run"]

export idempotent def cliHandles(subcommand: string): boolean {
  """
  Check whether the in-process Agency command helper supports a subcommand.
  """
  return CLI_SUBCOMMANDS.includes(subcommand)
}

/**
  Runs supported file commands through the matching functions in this
  module, without spawning an installed Agency CLI. Supports test,
  typecheck/tc, ast, fmt, and run. The run command selects node main.
  The fmt command returns formatted source without writing it back.
  Use the underlying functions when you need flags or other options.
*/
export def cli(args: string[], cwd: string = "."): Result<ExecResult> {
  """
  Run a supported Agency file command and return stdout, stderr, and exitCode.

  @param args - Exactly a supported subcommand and filename, such as ["test", "x.test.json"].
  @param cwd - The directory containing the file argument.
  """
  const supported = CLI_SUBCOMMANDS.join(", ")
  return match(args) {
    [sub, file] if (file.startsWith("-")) => failure("cli: `${sub}` takes a file, not a flag (${file}); call the function behind it for options")
    ["test", file] => cliTest(cwd, file)
    ["typecheck", file] => cliTypecheck(cwd, file)
    ["tc", file] => cliTypecheck(cwd, file)
    ["run", file] => cliRun(cwd, file)
    ["ast", file] => cliAst(cwd, file)
    ["fmt", file] => cliFmt(cwd, file)
    [sub, ...rest] if (cliHandles(sub)) => failure("cli: `${sub}` takes exactly one argument, the file; got ${rest.length}")
    [sub, ...rest] => failure("cli: no function runs `agency ${sub}`; the subcommands that run here are ${supported}")
    _ => failure("cli: no subcommand given; the subcommands that run here are ${supported}")
  }
}

// Each wrapper returns the underlying failure as it is, so its data,
// checkpoint, and origin reach the caller.
def cliTest(cwd: string, file: string): Result<ExecResult> {
  const report = testFile(cwd, file)?
  return success(renderTestReport(report))
}

def cliTypecheck(cwd: string, file: string): Result<ExecResult> {
  const report = typecheckFile(cwd, file)?
  return success(renderTypeCheck(report))
}

def cliRun(cwd: string, file: string): Result<ExecResult> {
  const value = runFile(cwd, file, "main")?
  return success(cliOutput(JSON.stringify(value)))
}

def cliAst(cwd: string, file: string): Result<ExecResult> {
  const source = read(file, cwd)?
  const tree = parseAST(source)?
  return success(cliOutput(JSON.stringify(tree, null, 2)))
}

def cliFmt(cwd: string, file: string): Result<ExecResult> {
  const source = read(file, cwd)?
  const formatted = format(source)?
  return success(cliOutput("${formatted}"))
}

def cliOutput(stdout: string): ExecResult {
  return {
    stdout: stdout,
    stderr: "",
    exitCode: 0
  }
}

def renderTestReport(report: TestReport): ExecResult {
  let lines: string[] = []
  let passed = 0
  for (c in report.cases) {
    if (c.pass) {
      passed = passed + 1
      lines.push("✓ ${c.node}")
    } else {
      lines.push("✗ ${c.node}\n${c.feedback}")
    }
  }
  lines.push("${passed}/${report.cases.length} tests passed")
  let exitCode = 1
  if (report.pass) {
    exitCode = 0
  }
  return {
    stdout: lines.join("\n") + "\n",
    stderr: "",
    exitCode: exitCode
  }
}

def renderTypeCheck(report: any): ExecResult {
  let lines: string[] = []
  for (d in report.errors) {
    lines.push("error ${d.code}: ${d.message}")
  }
  for (d in report.warnings) {
    lines.push("warning ${d.code}: ${d.message}")
  }
  if (lines.length == 0) {
    lines.push("No type errors found.")
  }
  let exitCode = 0
  if (report.errors.length > 0) {
    exitCode = 1
  }
  return {
    stdout: lines.join("\n") + "\n",
    stderr: "",
    exitCode: exitCode
  }
}
