Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

180 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

kwwk

A Swift-native coding agent with two faces:

  • kwwk — an interactive coding CLI (TUI) that drives your existing Anthropic, ChatGPT (Codex), GitHub Copilot, Cursor, or Kimi For Coding subscription — or an API key for Anthropic, OpenAI, Google (Gemini), OpenRouter, the Z.AI GLM Coding Plan, or any OpenAI-compatible endpoint.
  • KWWKAgent / KWWKAI — the agent runtime underneath, exposed as SwiftPM libraries so you can embed it in your own app, build custom tools, or swap the LLM provider.

Requirements

  • macOS 14+ runtime; Homebrew release bottles target macOS 15+ on Apple Silicon and Intel.
  • A bottled Homebrew install has no Swift or Xcode runtime dependency.
  • Building from source requires the Swift 6.1 toolchain (Xcode 16.3+ or the matching swift toolchain).

1. The coding CLI

Install

From Homebrew (recommended):

brew install EYHN/tap/kwwk

Or build from source:

swift build -c release --product kwwk
bin_dir="$(swift build -c release --show-bin-path)"
sudo install -d /usr/local/libexec/kwwk /usr/local/bin
sudo install -m 0755 "$bin_dir/kwwk" /usr/local/libexec/kwwk/kwwk
sudo cp -R "$bin_dir/kwwk_KWWKAI.bundle" /usr/local/libexec/kwwk/
printf '%s\n' '#!/bin/sh' 'exec /usr/local/libexec/kwwk/kwwk "$@"' \
  | sudo tee /usr/local/bin/kwwk >/dev/null
sudo chmod 0755 /usr/local/bin/kwwk

The resource bundle must stay beside the real executable. The launcher above executes that path directly; replacing it with a symlink can make SwiftPM look for kwwk_KWWKAI.bundle beside the symlink instead.

Run

kwwk              launch the interactive coding TUI
kwwk --help       show this message

Credentials come from the OAuth store at ~/.kwwk/oauth.json; if no login exists, the CLI checks supported API-key environment variables. With neither configured, kwwk starts logged out — launch it and run /login to sign in to a provider (OAuth subscription like ChatGPT Codex, Copilot, Claude Code, Cursor, or Kimi For Coding; or an API key for Anthropic, OpenAI, Google (Gemini), OpenRouter, the Z.AI GLM Coding Plan, or any OpenAI-compatible endpoint).

Inside the TUI, /help lists slash commands (/model, /thinking, /clear, …). The agent ships with Bash, Read, Write, Edit, Grep, Find, LS, and background-task tools out of the box.

Image inputs are resized and recompressed before entering the conversation.


2. The agent SDK

Add kwwk as a SwiftPM dependency:

.package(url: "https://github.com/EYHN/kwwk", branch: "main"),

Then depend on the libraries you need:

.product(name: "KWWKAgent", package: "kwwk"),
.product(name: "KWWKAI",    package: "kwwk"),
  • KWWKAI — model clients, provider registry, streaming, OAuth, message / tool types.
  • KWWKAgent — the turn/tool loop, built-in coding tools, hooks.

The SDK does not read ~/.kwwk or process environment variables by default. Pass credentials, session stores, context files, and skill directories explicitly. The kwwk binary is the layer that opts into ~/.kwwk/* and environment-key discovery.

Quick start — one-shot run

Agent.runOnce mirrors query() in the Python Agent SDK: a fresh agent runs a single prompt and yields every event as an async stream.

import KWWKAI
import KWWKAgent

// 1. Register a provider using an API key.
let anthropicAPIKey = "sk-ant-..."
await registerBuiltins(anthropic: anthropicAPIKey)

// 2. Build a coding agent scoped to a working directory.
let agent = await makeCodingAgent(CodingAgentConfig(
    model: Models.claudeSonnet5,
    cwd: FileManager.default.currentDirectoryPath,
    tools: .readOnly,
    bashEnvironment: [:]
)).agent

// 3. Drive it.
try await agent.prompt("Summarize the Swift files under Sources/KWWKAgent.")

// 4. Read the transcript.
for message in agent.state.messages {
    print(message)
}

Subagents

CodingAgentConfig.subagents defaults to an empty array. When it is empty, makeCodingAgent does not register the agent tool. Add subagent definitions explicitly when you want model-driven delegation:

let reviewer = SubagentDefinition(
    name: "reviewer",
    description: "Use for code quality, security, maintainability, and test coverage review.",
    prompt: """
    You are a senior code reviewer. Review code carefully, do not edit files,
    and report findings with file paths, severity, and concrete evidence.
    """,
    tools: .readOnly,
    model: .inherit
)

let shellEnvironment = ["PATH": "/usr/bin:/bin:/usr/sbin:/sbin"]
let coding = await makeCodingAgent(CodingAgentConfig(
    model: Models.claudeSonnet5,
    cwd: FileManager.default.currentDirectoryPath,
    tools: .standard,
    subagents: [reviewer],
    bashEnvironment: shellEnvironment
))

try await coding.agent.prompt("Use the reviewer subagent to review Sources/KWWKAgent.")

// A BackgroundTaskManager is created by default. Completed background tasks
// auto-continue the agent (new LLM runs start on their own). Call
// `coding.detachBackground?()` to unsubscribe when embedding, or pass
// `backgroundManager: nil` to disable background execution entirely.

SDK users can enable the same built-ins that the CLI uses without copying prompts:

let agent = await makeCodingAgent(CodingAgentConfig(
    model: Models.claudeSonnet5,
    cwd: FileManager.default.currentDirectoryPath,
    tools: .standard,
    bashEnvironment: shellEnvironment
).withBuiltinSubagents([.general, .explore, .plan, .codeReviewer, .testRunner])).agent

SDK users can also run a subagent directly:

let runner = SubagentRunner(
    cwd: FileManager.default.currentDirectoryPath,
    subagents: [.plan()],
    parentModel: Models.claudeSonnet5,
    parentTools: .readOnly,
    bashEnvironment: [:]
)
let result = try await runner.run(
    type: "Plan",
    prompt: "Plan how to simplify Sources/KWWKAgent/SubagentTool.swift."
)

Subagents are fresh-context agents: they do not inherit the parent transcript. The parent model must put the relevant files, errors, goals, and constraints into the agent tool's prompt. Trusted project context files and visible skill metadata are rebuilt into the child system prompt. Child coding tools are always capped by the parent's current coding-tool set; an explicit definition can narrow that set, but cannot expand it. The parent's beforeToolCall and afterToolCall policy/audit hooks are propagated to child tools. Conversation-specific hooks such as betweenTurns, transformContext, convertToLlm, and userPromptSubmit remain local to the parent.

Each agent surface defaults to four active children, one active child with write/edit/bash capability, 64 launches for the parent lifetime, 16 child turns, and a 600-second child deadline. Configure these through SubagentLimits. Model-issued overrides must name the parent model, a same-provider catalog model, or a host-approved allowedSubagentModels entry; programmatic SubagentModel.override remains the trusted host path for custom models. Child completion uses an internal, structured subagent_yield contract: a plain provider stop is not treated as success. A child that forgets to yield receives at most three internal reminders; the final reminder exposes only the yield tool. Missing or explicit incomplete yields are reported as incomplete and retain usage, cost, duration, turns, and bounded untrusted salvage when available.

Each subagent run gets its own child session id. Tools inside that subagent, including background-capable tools such as Bash, are scoped to the child session. While the child agent is running, background task notifications are attached to that child session. When the subagent finishes or is cancelled, the generic background-task session is closed: still-running tasks in that child session are killed and queued notifications for that child session are discarded. If the parent starts the subagent itself with run_in_background, that top-level subagent task remains parent-visible to task_poll and automatic runtime completion notifications. Normal completion is delivered automatically; task_poll is only for a parent that is otherwise blocked, and one call can watch multiple task ids with wait-any semantics.

When background execution is available, makeCodingAgent registers a parent-only agent_history tool that pages a background subagent's retained messages by task id. Use task_list to discover task ids. Internal child session ids are not exposed to the model. The registry is process-local, keeps at most the newest 32 terminal children subject to a 16 MiB estimated transcript budget, and does not survive application restart. Each response is capped at 64 KiB and marks an individual message that is too large for one response. SDK users who construct createAgentTool directly can share a SubagentHistoryStore with createSubagentHistoryTool; SubagentRunner.historyStore exposes the same process-local registry for direct-run integrations.

In the interactive TUI, foreground subagent tool calls update their in-flight display with the child agent's token usage as it runs. When a provider does not stream exact usage until the end of the turn, the live counter falls back to an approximate output-token estimate and is replaced by provider-reported usage once available.

Subagent tools also emit structured runtime events through AgentEvent.runtimeEvent(.subagent(...)): started, tool update, background started, completed, and failed. The terminal AgentRunSummary.subagents array records each foreground child run's usage, cost, turns, duration, status, model, and child session id. Background subagents are recorded when the parent-visible background task is started; their terminal completion/failure is emitted later as the same SubagentLifecycleEvent, correlated by background task id and child session id, independently of whether a runtime aside or task_poll consumes the model-facing notification. Background-task snapshots retain the structured outcome, including usage and cost. agent.backgroundSubagentRuns() exposes the terminal cross-run aggregate to SDK hosts.

The interactive kwwk CLI enables five built-ins by default: explore, plan, code-reviewer, test-runner, and general. subagent_type is required, and the tool description orders narrower specialists before general; there is no silent fallback to a full-power child. general inherits the parent agent's tools and is reserved for implementation work. explore, plan, and code-reviewer are read-only specialists. test-runner has Bash but enforces a conservative runtime policy: exactly one direct build/test process per tool call; shell composition, redirection, command substitution, cleanup arguments, and unrelated executables are rejected before spawn. This is an accidental-destruction boundary, not an OS sandbox—the selected build system still executes trusted project code. Interactive CLI built-ins default to background execution so independent team fan-out does not turn the parent into a wait-all barrier; pass run_in_background: false when the parent must block for one result. agent_history({"task_id":"..."}) exposes a child's live transcript while parent work remains. task_list({}) exposes live status plus a bounded progress/output tail, and completion is delivered as an internal runtime aside rather than an editable user queue item. Use --no-subagents to disable them or --subagents read-only or --subagents general,test-runner to enable only a subset. The SDK does not enable those automatically. readOnly is a tool whitelist, not an operating-system filesystem sandbox. The built-in explore and plan definitions additionally use canonical workspace path containment for read/grep/find/ls (including .. and symlink checks). That path policy still does not constrain Bash/custom tools and is not an OS-level sandbox or a defense against hostile concurrent symlink replacement.

One-shot kwwk -p exposes the same background-task and background Bash capabilities while its top-level Agent loop is running. It does not wait for background-only work or start a fresh model run after the loop becomes idle: when that loop returns, headless teardown retires the Agent, kills unfinished tasks, and exits.

When an SDK application is done with an agent session, call await agent.closeSession(). This permanently stops the agent, kills its active background tasks, waits for the current run to finish cancelling, and releases provider-owned resources keyed by that session id. For OpenAI Responses WebSocket transport, this also closes the stored WebSocket connection. Use await agent.stop() for the same deterministic agent/task shutdown without closing provider session resources.

Streaming events

Subscribe before prompting to observe tokens, tool calls, and the final summary as they happen:

let unsubscribe = agent.subscribe { event, _ in
    switch event {
    case .messageUpdate(let assistant, _):
        // Live-render streaming assistant tokens.
        print(assistant.textPreview, terminator: "")
    case .toolExecutionStart(_, let name, let args):
        print("\(name) \(args)")
    case .agentEnd(_, let summary):
        print("\n[\(summary.turns) turns · $\(summary.cost.total)]")
    default: break
    }
}
defer { unsubscribe() }

try await agent.prompt("Find all TODOs in this repo.")

Or consume runOnce as an AsyncThrowingStream:

for try await event in Agent.runOnce(
    prompt: "what's in README.md?",
    options: AgentOptions(initialState: AgentInitialState(
        model: Models.claudeHaiku45,
        tools: [createReadTool(cwd: ".")]
    ))
) {
    if case .messageEnd(let message) = event { print(message) }
}

Custom tools

A tool is a name, a JSON-Schema parameter spec, and an async execute closure. The agent handles validation, cancellation, and wiring the result back into the transcript.

import KWWKAI
import KWWKAgent

let weather = AgentTool(
    name: "get_weather",
    label: "weather",
    description: "Look up the current temperature for a city.",
    parameters: [
        "type": "object",
        "properties": [
            "city": ["type": "string", "description": "City name"]
        ],
        "required": ["city"]
    ],
    execute: { _, args, _, _ in
        guard case .object(let obj) = args,
              case .string(let city) = obj["city"] ?? .null else {
            throw CodingToolError.invalidArgument("city required")
        }
        let temp = try await fetchTemp(city)
        return AgentToolResult(content: [.text(.init(text: "\(temp)°C in \(city)"))])
    }
)

let agent = Agent(initialState: AgentInitialState(
    model: Models.claudeSonnet5,
    tools: [weather]
))
try await agent.prompt("Is it warmer in Tokyo or Oslo right now?")

Hooks — audit, redact, short-circuit

Every AgentOptions accepts hooks that fire at well-defined points. Use them to enforce policy without forking the loop:

let options = AgentOptions(
    initialState: AgentInitialState(model: Models.claudeSonnet5, tools: [...]),
    // Block or rewrite a tool call before it runs.
    beforeToolCall: { ctx, _ in
        if ctx.toolCall.name == "bash",
           case .object(let o) = ctx.args,
           case .string(let cmd) = o["command"] ?? .null,
           cmd.contains("rm -rf") {
            return BeforeToolCallResult(block: true, reason: "destructive command blocked")
        }
        return nil
    },
    // Intercept a user prompt before it enters the transcript.
    userPromptSubmit: { ctx, _ in
        // e.g. redact secrets, inject policy preamble.
        return nil
    }
)
let agent = Agent(options: options)

Other hook points: afterToolCall, convertToLlm, transformContext (for context pruning / summarization).

Context compaction

AgentOptions.autoCompact defaults to a 75% threshold, matching CodingAgentConfig.autoCompactThreshold, standalone subagent SDK entry points, and the CLI. Pass nil explicitly to disable both proactive compaction and provider-overflow recovery. Compaction turns older history into a structured, incrementally updated recap while keeping recent turns verbatim. The budget includes the system prompt and tool schemas, preserves tool-call / result boundaries, and retries one provider-reported input overflow after rebuilding the request. Manual /compact uses the same projection pipeline.

Set AgentOptions.compactionModel (or CodingAgentConfig.compactionModel) to send summary-generation requests to a different model. Context thresholds, recovery targets, and post-compaction validation still use the live conversation model. Assign nil to follow the live model dynamically. In the TUI, use /compact-model to pick an authenticated model, /compact-model status to inspect it, or /compact-model clear to follow /model again. A custom streamFn must route each request using the Model argument it receives. AgentContextCompactionConfig.summaryMaxTokens defaults to 0, which leaves the summary stream cap automatic; set a positive value only when an explicit hard output limit is required.

Steering a running agent

Queue a message that will be injected at the next turn boundary — without aborting the current turn:

Task {
    try await agent.prompt("refactor this module end-to-end")
}

// later, from any thread:
agent.steer("also add tests as you go")

Providers

registerBuiltins covers Anthropic, OpenAI (Completions + Responses), and Google Gemini from explicit keys. For CLI-style environment discovery, call registerBuiltinsFromEnvironment(env:) with an environment snapshot. Models exposes a small curated catalog (claudeSonnet5, gpt55, gemini35Flash, …) or you can construct Model values by hand. For OpenAI-compatible endpoints (xAI, Groq, OpenRouter) there are Models.xaiGrok(id:), Models.groq(id:), Models.openRouter(id:) helpers.

To use a subscription (OAuth) token instead of a raw API key, drive the flow via KWWKAI.OAuth / OAuthLogin — the same code path the CLI's in-session /login command uses.

Updating the model catalog

There are two bundled catalogs, and a sync should regenerate BOTH — don't update one without the other:

  1. Sources/KWWKAI/Resources/models.json — every regular provider, generated from pi-mono's packages/ai/src/models.generated.ts.
  2. Sources/KWWKAI/Resources/cursor-models.json — the Cursor subscription models, pulled live from Cursor's GetUsableModels RPC (there is no runtime model sync; this file is the authoritative Cursor catalog).
# In the pi-mono checkout, materialize the generated provider JSON first.
node packages/ai/scripts/generate-models.ts

# In the kwwk checkout, use that exact pi-mono checkout as the input.
swift run kwwk-generate-models /path/to/pi-mono/packages/ai/src/models.generated.ts
swift run kwwk-generate-cursor-models
swift test

Current pi-mono provider catalogs import their values from generated packages/ai/src/providers/data/*.json files. Those files are intentionally Git-ignored upstream, so the pi-mono generator must run in that checkout before kwwk-generate-models. Older inline provider catalogs remain supported.

kwwk-generate-cursor-models authenticates via CURSOR_ACCESS_TOKEN, an existing cursor login in ~/.kwwk/oauth.json, or — with neither present — an interactive browser login it persists for next time.

The catalog tests assert unsupported Google Gemini CLI and Google Antigravity provider groups stay absent.


Layout

  • Sources/KWWKAI — model clients, OAuth, provider adapters
  • Sources/KWWKAgent — tool-using agent loop and built-in tools
  • Sources/KWWKCli — interactive TUI, slash commands, rendering
  • Sources/kwwk — the executable entry point
  • Tests/ — XCTest suites for each module

Run the full package test suite with SwiftPM:

swift test

A note on OAuth client IDs

Sources/KWWKAI/OAuthProviders.swift reuses the OAuth client IDs (and, where applicable, public app metadata) shipped by the upstream first-party CLIs — Anthropic's Claude Code, OpenAI's Codex CLI, and GitHub Copilot's VS Code extension. Those credentials are not secrets in any meaningful sense — they are embedded in those open-source CLIs and are required for the "log in with your existing subscription" flow to work. They remain the property of their respective vendors, who may rotate or revoke them at any time. kwwk is not affiliated with or endorsed by any of these vendors.

License

MIT — see LICENSE.

About

No description, website, or topics provided.

Resources

Stars

122 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages