Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

50 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Fathom

CI

Fathom the depths, surface the answer.

A small, dependency-free Swift agent SDK for DeepSeek-style (OpenAI-compatible) models. Define an Agent — a model + system prompt + tools + policies — and it drives a full tool-calling loop to an answer. Not just an API wrapper: it ships the machinery a real agent needs.

What you get

  • Agent + Thread — a reusable agent, and stateful multi-turn conversations with memory; optional context compaction summarizes old turns once the transcript grows past a contextLimit, so long-running conversations stay within the window.
  • Plan → Act → Verify — optional planning (decompose the goal into steps first) and a critic (review the draft answer and revise once if it falls short) — a thinking agent.
  • Tool-calling loop with production safety rails:
    • No repeated tool calls — identical calls (even with reordered JSON keys) are collapsed.
    • No-progress cap — two rounds that add nothing new force a final answer.
    • Round cap — a hard ceiling on tool-calling rounds.
  • Human-in-the-loop approval — mutating tools are gated through an approval hook; deny with a reason and the model adapts. Read-only tools never prompt.
  • Parallel tool execution — independent tool calls in one round run concurrently.
  • Observation hook — collect side effects (citations, traces) as the loop runs.
  • Resilience — wrap any client in RetryingClient for retry + backoff on transient failures.
  • Cost control & cancellation — cumulative token usage on every RunResult, an optional tokenBudget that stops the loop early (finish .budget), and cooperative Task cancellation (returns best-so-far, finish .cancelled).
  • Output guardrails — validate the final answer with a host predicate; on failure it auto-regenerates with the reason fed back (bounded by maxGuardrailRetries). Enforce JSON-parses, required fields, length, no-PII, etc.
  • Streamingagent.stream(query) yields AgentEvents (.status / .toolResult / .answerDelta / .reasoningDelta / .finished) so the answer arrives token-by-token; DeepSeekClient streams via SSE, and any non-streaming client falls back to a whole-answer yield.
  • In-run context compaction — a long tool run summarizes the middle of its own transcript (goal and recent tail kept verbatim, tool results never orphaned) and keeps working instead of overflowing the context window. Set compactionThresholdTokens; works in streaming runs too.
  • Structured outputrunStructured(MyType.self, …) returns the final answer decoded as a Swift Decodable, with malformed JSON regenerated automatically (the decode error is fed back). Turns an agent into a typed pipeline stage.
  • Thinking mode (deepseek-v4) — opt in with LLMConfig.thinking + reasoningEffort; the SDK handles the API contract (tool-call turns return their reasoning_content verbatim, streaming surfaces reasoning deltas for a "thinking…" UI).
  • Checkpoint / resumeonCheckpoint emits a Codable RunCheckpoint after every tool round; persist it (FileCheckpointStore) and a killed or crashed run continues from its last round via resume(_:tools:) — transcript, de-dup memory, and counters intact.
  • Coding agent, sandboxedFileSandbox.codingTools() gives the loop read/write/edit/list/ glob/grep/run-command over one directory; commands can run under a macOS sandbox profile (no network, writes confined to the workdir). Point it at a folder and the agent writes & runs code to build deliverables.
  • Sub-agents — wrap any Agent as a SubAgentTool, or expose a menu of named specialists through SubAgentRouterTool (the agentic fan-out primitive) so a parent agent delegates focused sub-tasks (hierarchical / multi-agent workflows).
  • Built-in general toolsCalculatorTool, UnitConvertTool, CurrentDateTimeTool, TranslateTool, plus WebSearchTool/WebFetchTool (host supplies a WebSearchEngine), ready to drop into any agent; bring your own for app-specific capabilities.

Fully mockable (inject any LLMClient) — the whole thing is testable offline, no network.

Install

// Package.swift
dependencies: [
    .package(url: "https://github.com/<you>/Fathom.git", from: "1.0.0")
],
targets: [
    .target(name: "MyApp", dependencies: ["Fathom"])
]

Usage

import Fathom

let client = DeepSeekClient(config: LLMConfig(apiKey: "sk-…"))

let search = ClosureTool(
    name: "search",
    description: "Search the knowledge base",
    parameters: ["type": "object", "properties": [
        "query": ["type": "string", "description": "what to look for"]
    ]]
) { argumentsJSON in
    // decode argumentsJSON, do the work, return a textual result
    "…results…"
}

let agent = Agent(
    client: client,
    systemPrompt: "You are a helpful research assistant.",
    tools: [search]
)

let result = try await agent.run("What changed in the Q3 report?")
print(result.answer)         // the model's final answer
print(result.toolCallCount)  // how many tools ran
print(result.finish)         // .natural / .noProgress / .roundLimit

Multi-turn conversations

let thread = agent.thread()
_ = try await thread.send("Summarize the report.")
_ = try await thread.send("Now compare it to last year.")   // remembers the first turn
print(thread.messages)   // the running transcript

Human-in-the-loop approval

Mutating tools (isMutating: true) are gated through approval; read-only tools never prompt.

let agent = Agent(
    client: client, systemPrompt: "", tools: [deleteTool],
    approval: { call in
        await userConfirms(call) ? .allow : .deny("user declined")
    }
)

Plan → Act → Verify

Turn on planning (decompose first) and the critic (review + revise) for a thinking agent:

let agent = Agent(client: client, systemPrompt: "", tools: tools,
                  planning: true, critic: true)
let result = try await agent.run("Compare last two quarterly reports and flag risks.")
print(result.plan)      // the steps it decomposed the goal into
print(result.revised)   // true if the critic forced a revision

Typed answers (structured output)

struct Report: Decodable { let title: String; let risks: [String] }

let (report, run) = try await agent.runStructured(Report.self,
    query: "Summarize Q3 and list the top risks.",
    schemaHint: #"{"title": string, "risks": [string]}"#)
print(report.risks)            // a real Swift value — no prose parsing
print(run.guardrailRetries)    // how many malformed answers were regenerated

A coding agent over one directory

let sandbox = FileSandbox(root: workdirURL)
let builder = Orchestrator(client: client, maxRounds: 16, planning: true,
                           compactionThresholdTokens: 100_000)   // survives past the window
let result = try await builder.run(
    systemPrompt: "You write and run code to produce the deliverable.",
    query: "Build a PDF report from data.csv",
    tools: sandbox.codingTools(commandTimeout: 180, sandboxed: true))  // no network, writes confined

Crash-resumable runs

let store = FileCheckpointStore(url: checkpointURL)
var orch = Orchestrator(client: client, onCheckpoint: { store.save($0) })
let result = if let saved = store.load() {
    try await orch.resume(saved, tools: tools)     // pick up at the last completed round
} else {
    try await orch.run(systemPrompt: sys, query: q, tools: tools)
}
store.clear()

Thinking mode (deepseek-v4)

let cfg = LLMConfig(apiKey: "sk-…", model: "deepseek-v4-pro",
                    thinking: true, reasoningEffort: "high")
// The SDK returns each tool turn's reasoning_content to the API as required,
// and streams `.reasoningDelta` events for a "thinking…" UI.

Resilience

let client = RetryingClient(wrapping: DeepSeekClient(config: cfg), maxAttempts: 4)

Collecting side effects as the loop runs

onObservation fires after every tool call (fresh or de-duplicated repeat) — the seam a host uses to gather citations or traces without owning the loop:

let agent = Agent(
    client: client, systemPrompt: "", tools: tools,
    onObservation: { obs in
        print(obs.toolName, obs.arguments, obs.isRepeat, obs.approved)
    }
)

API

Type Purpose
LLMClient Protocol for the chat endpoint — inject DeepSeekClient or a mock.
DeepSeekClient URLSession transport for DeepSeek's chat/completions (public wire/parseCompletion; SSE streaming; thinking mode).
OrchestratorTool / ClosureTool A capability the model can call.
Orchestrator The tool-calling ACT loop with safety rails, compaction, checkpointing, and hooks.
FileSandbox Directory-confined coding tools (read/write/edit/glob/grep/run, optional OS sandbox).
RunCheckpoint / FileCheckpointStore Resumable run snapshots and their simplest persistence.
SubAgentTool / SubAgentRouterTool Delegate sub-tasks to one specialist, or route across a menu of them.
ChatMessage / ToolCall / Completion / RunResult / FinishReason Chat primitives (Codable where persistence needs them).

Testing

swift test

The loop is exercised with a scripted mock LLMClient — no network or API key required.

License

Apache-2.0. See LICENSE.

About

A small, dependency-free Swift SDK for DeepSeek-style tool-calling agent loops (mockable LLMClient + Orchestrator with agent-loop safety rails).

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages