// The memory LLM calls return these structured shapes. Defining them in
// Agency lets the runtime derive the `responseFormat` Zod schema from
// the type annotation on `llm()`, so structured-output enforcement
// flows through the same path as user-defined typed prompts.

// `_*` exports read ctx from AsyncLocalStorage (see
// `lib/runtime/asyncContext.ts`); no `__ctx` arg is plumbed at the
// call site. The deprecated `__internal_*` exports remain in
// `lib/stdlib/memory.ts` for the migration window.
import {
  _setMemoryId,
  _getMemoryId,
  _shouldRunMemory,
  _buildExtractionPrompt,
  _applyExtractionResult,
  _buildForgetPrompt,
  _applyForgetResult,
  _recall,
  _enableMemory,
  _disableMemory,
  _pushMemoryFrame,
  _popMemoryFrame,
 } from "agency-lang/stdlib-lib/memory.js"

/** @module
@summary Give an agent long-term memory: remember extracts and saves facts to a knowledge graph, and recall retrieves them later.
Give an agent long-term memory. `remember` extracts facts from text
and saves them to a knowledge graph, and `recall` retrieves the
relevant ones later. Memory is off until you turn it on with
`enableMemory`, and it stays local to the current branch.

Memory reads and writes raise an approval interrupt, so call them
`with approve` (or your own policy) to let them through.

```ts
import { enableMemory, remember, recall } from "std::memory"

node main() {
  enableMemory({ dir: "./mem" }) with approve
  remember("Alice's favorite color is blue") with approve
  const facts: string = recall("What is Alice's favorite color?") with approve
  print(facts)
}
```
*/

type ExtractionResult = {
  entities: { name: string; type: string; observations: string[] }[];
  relations: { from: string; to: string; type: string }[];
  expirations: { entityName: string; observationContent: string }[]
}

type ForgetResult = {
  observations: { entityName: string; observationContent: string }[];
  relations: { fromName: string; toName: string; type: string }[]
}

// User-facing memory config shape. Mirrors the `memory:` block in
// `agency.json` so the same fields work whether you configure memory
// declaratively or in code. Every nested field is optional because
// the runtime provides defaults for each one (matching the shape of
// `MemoryConfig` in `lib/runtime/memory/types.ts`).
export type MemoryConfig = {
  dir: string;
  model?: string;
  autoExtract?: { interval: number | undefined };
  compaction?: { trigger: "token" | "messages" | undefined; threshold: number | undefined };
  embeddings?: { model: string | undefined; provider: string | undefined }
}

effect std::memory::enableMemory { dir: string }
effect std::memory::disableMemory {}
effect std::memory::remember { contentLength: number }
effect std::memory::recall { query: string }
effect std::memory::forget { query: string }

/** Useful for branching in user code (`if (isMemoryActive()) { ... }`)
 *  and for tests that verify memory is on without inspecting internal
 *  state. */
export def isMemoryActive(): boolean {
  """
  Return `true` when memory is on for the current branch and reads and
  writes will reach a real store. Returns `false` when memory was never
  enabled, was turned off, or the call is outside any runtime frame.
  """
  return _shouldRunMemory()
}

/** The id is independent of which memory configuration is active. It
 *  persists as memory is turned on and off. Re-set it explicitly when
 *  switching stores. Branch-scoped: a fork/race branch inherits the id
 *  active at fork time, and a change inside a branch stays local to that
 *  branch. */
export def setMemoryId(id: string) {
  """
  Set the memory scope for this agent run, so reads and writes target a
  specific user, thread, or workspace. Call before other memory
  operations. The scope defaults to "default" if never set. Memory must
  be enabled for this to have any effect.

  @param id - A unique identifier for the memory scope (e.g. user ID)
  """
  _setMemoryId(id)
}

export def getMemoryId(): string {
  """
  Return the current memory scope id, or "default" if it was never set
  or memory is not active.

  @returns The active memory scope id
  """
  return _getMemoryId()
}

/** Storage is shared process-wide by absolute directory, so calls
 *  (across runs, forks, or modules) that point at the same dir share one
 *  store. Enabling the same dir again is a no-op, so declaring
 *  `static const _ = enableMemory({...})` AND calling it from `main()` is
 *  safe. Enabling a different dir stacks on top. Turn it off with
 *  `disableMemory()` or use the block form for lexical scoping.
 *
 *  `config.dir` is resolved against `process.cwd()`, not the module dir.
 *  This deliberately mirrors how `agency.json`'s `memory.dir` resolves, so
 *  the same string in JSON and in code points at the same place.
 *
 *  Branch-scoped: a fork/race branch inherits the config active at fork
 *  time, and enabling/disabling inside a branch stays local to that
 *  branch. */
export def enableMemory(config: MemoryConfig) {
  """
  Turn memory on for the current execution branch using `config`. The
  directory in `config.dir` is created if missing. Enabling a directory
  that is already active is a no-op.

  @param config - Memory configuration with `dir` required
  """
  return interrupt std::memory::enableMemory("Are you sure you want to enable memory at this directory?", {
    dir: config.dir
  })

  _enableMemory(config)
}

/** Removes whatever memory configuration is on top, including a bottom
 *  frame seeded from `agency.json`. Library authors should not call this
 *  casually. It shadows the caller's configured memory. Prefer the block
 *  form `memory({...}) as { ... }`, which restores the previous
 *  configuration on exit. Branch-scoped: a call inside a fork branch stays
 *  local to that branch. */
export def disableMemory() {
  """
  Turn off memory for the current branch by removing the most recently
  enabled memory configuration.
  """
  return interrupt std::memory::disableMemory("Are you sure you want to disable memory (pop the top memory frame)?", {})

  _disableMemory()
}

export def memory(config: MemoryConfig, block: () -> any = null): Result {
  """
  Run `block` with `config` as the active memory configuration, then
  restore the previous configuration when the block returns or fails.
  Returns a `Result`: success holds the block's return value, failure
  holds an error raised inside the block.

  Example:
  const r = memory({ dir: "./mem-user-a" }) as {
  remember("alice's favorite color is blue")
  }

  @param config - Memory configuration with `dir` required
  @param block - The code to run with the configuration active
  """
  const pushed = _pushMemoryFrame(config)
  const result = try block()
  if (pushed) {
    _popMemoryFrame()
  }
  return result
}

export def remember(content: string) {
  """
  Extract structured facts (entities, observations, and relations) from
  the given text and store them in the knowledge graph.

  @param content - Natural language text containing facts to remember
  """
  if (_shouldRunMemory()) {
    return interrupt std::memory::remember("Are you sure you want to extract and persist facts to memory?", {
      contentLength: content.length
    })

    destructive {
      thread {
        const prompt = _buildExtractionPrompt(content)
        // Bang validation: a malformed extraction is a failure Result, not
        // a mistyped value. Before this, a provider returning prose or an
        // empty string crashed the tool and memory silently stopped
        // persisting (issue #494).
        const result: ExtractionResult! = llm(prompt)
        if (result is success(extraction)) {
          _applyExtractionResult(extraction)
        } else {
          return result
        }
      }
    }
  }
}

export def recall(query: string): string {
  """
  Retrieve relevant facts from the knowledge graph as a formatted
  string. Combines structured lookup, embedding similarity, and
  LLM-powered retrieval. Returns up to 10 entities ranked by match
  quality.

  Returns an empty string if memory is not configured or nothing matches.

  @param query - A natural language query describing what to recall
  """
  return interrupt std::memory::recall("Are you sure you want to recall memories based on this query?", {
    query: query
  })

  return _recall(query)
}

export def forget(query: string) {
  """
  Soft-delete facts matching the query from the knowledge graph. Data is
  not erased. Affected observations are marked with a validTo timestamp,
  preserving the audit trail.

  @param query - A natural language description of what to forget
  """
  if (_shouldRunMemory()) {
    return interrupt std::memory::forget("Are you sure you want to soft-delete facts matching this query from memory?", {
      query: query
    })

    thread {
      const prompt = _buildForgetPrompt(query)
      // Bang validation, same contract as remember: malformed output is
      // a failure Result the caller can see, never a crash.
      const result: ForgetResult! = llm(prompt)
      if (result is success(forgetPlan)) {
        _applyForgetResult(forgetPlan)
      } else {
        return result
      }
    }
  }
}
