7 Commits
Author SHA1 Message Date
owen 71f84c03ce Add Rust embedding API
CI / Build (push) Waiting to run
CI / Test (push) Waiting to run
2026-06-25 06:53:13 -05:00
owen 4d021fe2ab Resolve lru advisory for v0.2.5
CI / Build (push) Waiting to run
CI / Test (push) Waiting to run
2026-06-25 05:53:53 -05:00
owen 7c8cc9eac5 Adding a security policy 2026-06-25 05:39:37 -05:00
IrrelevantandGitHub 52c879e7c8 Merge pull request #2 from owenqwenstarsky/add-github-issue-templates
Add GitHub issue templates
2026-06-24 20:58:19 -05:00
owen 60dfdf3806 Add GitHub issue templates 2026-06-24 20:57:00 -05:00
owen 8ffc5284e3 Refine system prompt for v0.2.4
CI / Build (push) Waiting to run
CI / Test (push) Waiting to run
2026-06-24 20:46:27 -05:00
owen 7f0f1619af Update roadmap planned release section 2026-06-24 03:33:23 -05:00
25 changed files with 3436 additions and 221 deletions
+101
View File
@@ -0,0 +1,101 @@
name: Bug report
description: Report a reproducible problem with Cassady / cass
title: "bug: "
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thanks for reporting a bug. Please include enough detail for someone to reproduce it.
- type: textarea
id: summary
attributes:
label: Summary
description: What went wrong?
placeholder: Cassady crashes when...
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
description: List the exact commands, key presses, or config changes needed to trigger the issue.
placeholder: |
1. Run `cass ...`
2. Press ...
3. See ...
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: What did you expect to happen?
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual behavior
description: What happened instead? Paste errors, logs, or terminal output when useful.
render: text
validations:
required: true
- type: input
id: version
attributes:
label: Cassady version
description: Output of `cass --version` or `cassady --version`.
placeholder: cassady 0.2.x
validations:
required: true
- type: dropdown
id: install
attributes:
label: Install method
options:
- Release archive
- cargo install --git
- cargo install --path / local build
- Other
validations:
required: true
- type: dropdown
id: platform
attributes:
label: Platform
options:
- macOS Apple Silicon
- macOS Intel
- Linux x86_64
- Linux ARM64
- Windows x86_64
- Other
validations:
required: true
- type: textarea
id: environment
attributes:
label: Environment details
description: Terminal, shell, provider/model, access mode, and anything notable from `cass check`. Do not include API keys.
placeholder: |
Terminal: ...
Shell: ...
Provider/model: ...
Access mode: ...
`cass check`: ...
- type: textarea
id: config
attributes:
label: Relevant configuration
description: Paste sanitized snippets from `~/.cass/config.json`, `providers.json`, or `models.json` if relevant. Remove API keys and secrets.
render: json
- type: checkboxes
id: checklist
attributes:
label: Checklist
options:
- label: I removed API keys, tokens, and other secrets from this report.
required: true
- label: I searched existing issues for a duplicate.
required: true
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Questions and usage help
url: https://github.com/owenqwenstarsky/cassady/discussions
about: Ask questions, share setup notes, or discuss ideas before filing an issue.
+44
View File
@@ -0,0 +1,44 @@
name: Documentation issue
description: Report missing, confusing, or incorrect documentation
title: "docs: "
labels: [documentation]
body:
- type: textarea
id: location
attributes:
label: Documentation location
description: Link to the page or name the file/section, if known.
placeholder: README.md, docs/providers.md, docs/access-modes.md, ...
validations:
required: true
- type: textarea
id: issue
attributes:
label: What is unclear or incorrect?
description: Describe the gap, mistake, or confusing wording.
validations:
required: true
- type: textarea
id: suggested
attributes:
label: Suggested improvement
description: If you have specific wording or examples in mind, add them here.
- type: dropdown
id: impact
attributes:
label: Impact
options:
- Blocks installation or first use
- Causes incorrect configuration
- Confuses normal usage
- Typo or small polish
- Other
validations:
required: true
- type: checkboxes
id: checklist
attributes:
label: Checklist
options:
- label: I searched existing issues for a duplicate.
required: true
@@ -0,0 +1,59 @@
name: Feature request
description: Suggest an improvement or new capability for Cassady / cass
title: "feat: "
labels: [enhancement]
body:
- type: markdown
attributes:
value: |
Thanks for suggesting an improvement. Please focus on the user problem and desired outcome.
- type: textarea
id: problem
attributes:
label: Problem or use case
description: What are you trying to do, and what makes it hard today?
placeholder: I want to...
validations:
required: true
- type: textarea
id: proposal
attributes:
label: Proposed behavior
description: Describe the change you would like to see.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: What workarounds or other designs have you considered?
- type: dropdown
id: area
attributes:
label: Area
options:
- Terminal UI
- Setup and configuration
- Providers and models
- Tools and file editing
- Safety and access modes
- Conversation history and resume
- Documentation
- Packaging and releases
- Other
validations:
required: true
- type: textarea
id: examples
attributes:
label: Examples or mockups
description: Add sample commands, UI text, config snippets, screenshots, or links that clarify the request.
- type: checkboxes
id: checklist
attributes:
label: Checklist
options:
- label: I searched existing issues for related requests.
required: true
- label: This request is in scope for a terminal coding agent, not a general package manager or updater.
required: false
+52
View File
@@ -0,0 +1,52 @@
name: Maintenance task
description: Track internal cleanup, refactoring, testing, CI, or release work
title: "chore: "
labels: [maintenance]
body:
- type: textarea
id: goal
attributes:
label: Goal
description: What should be done, and why?
validations:
required: true
- type: textarea
id: scope
attributes:
label: Scope
description: List concrete files, modules, or workflows that are in scope.
placeholder: |
- src/...
- tests/...
- .github/workflows/...
validations:
required: true
- type: textarea
id: out_of_scope
attributes:
label: Out of scope
description: Note anything this task should intentionally avoid.
- type: textarea
id: acceptance
attributes:
label: Acceptance criteria
description: What must be true before this can be closed?
placeholder: |
- [ ] ...
- [ ] `cargo test --locked --all-targets` passes
validations:
required: true
- type: dropdown
id: area
attributes:
label: Area
options:
- Refactoring
- Tests
- CI
- Release process
- Dependencies
- Documentation maintenance
- Other
validations:
required: true
Generated
+849 -103
View File
File diff suppressed because it is too large Load Diff
+3 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "cassady"
version = "0.2.3"
version = "0.2.6"
edition = "2021"
description = "Cassady/Cass minimal terminal coding agent"
license = "MIT"
@@ -23,13 +23,13 @@ anyhow = "1"
async-trait = "0.1"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4", features = ["derive"] }
crossterm = "0.28"
crossterm = "0.29"
dirs = "5"
futures-util = "0.3"
ignore = "0.4"
include_dir = "0.7"
nanoid = "0.4"
ratatui = "0.28"
ratatui = { version = "0.30", default-features = false, features = ["crossterm", "underline-color"] }
regex = "1"
pulldown-cmark = "0.12"
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "stream"] }
@@ -37,7 +37,6 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync", "time", "process", "io-util"] }
tui-textarea = "0.6"
unicode-width = "0.1"
[dev-dependencies]
+19 -1
View File
@@ -8,6 +8,7 @@ The project installs two equivalent commands, `cass` and `cassady`; examples use
- Provider support is OpenAI-compatible chat/completions APIs only.
- The primary interface is an interactive terminal UI.
- v0.2.6 adds an experimental Rust embedding API for headless sessions; it is useful for early integrations but not yet a stable long-term library contract.
- Config and conversation state live under `~/.cass`.
- Windows binaries are built for releases, but deeper Windows terminal, path, shell, and filesystem polish is planned for a later release.
- Cassady is not an installer, updater, or package manager.
@@ -105,19 +106,36 @@ Cassady stores user-editable files in `~/.cass`:
- `config.json`: active defaults and preferences.
- `providers.json`: provider base URLs and API key references.
- `models.json`: model metadata.
- `global.md`: optional global instructions added to new chats.
- `global.md`: optional global instructions added to new chat system prompts when they fit the active request; they cannot override access modes, tool denials, approvals, or workspace boundaries.
- `docs/`: bundled documentation installed from the current binary.
API key references should usually be written as environment variables such as `"$OPENAI_API_KEY"`.
Detailed bundled docs live in this repository under [`docs/`](docs/README.md) and are installed to `~/.cass/docs` at runtime.
## Experimental Rust embedding API
Rust applications can import Cassady and run headless sessions without launching the TUI:
```rust
use cassady::prelude::*;
let session = SessionBuilder::new()
.cwd(".")
.access_mode(AccessMode::ReadOnly)
.build()
.await?;
```
See [Experimental Rust embedding API](docs/embedding.md) for session creation, streamed events, approval handling, cancellation, and current limitations.
## More documentation
- [Commands](docs/commands.md)
- [Configuration](docs/configuration.md)
- [Providers and models](docs/providers.md)
- [Access modes and tool safety](docs/access-modes.md)
- [Experimental Rust embedding API](docs/embedding.md)
- [Workflows](docs/workflows.md)
- [Troubleshooting](docs/troubleshooting.md)
- [Platform notes](docs/platforms.md)
+164 -76
View File
@@ -1,102 +1,92 @@
# Cassady (Cass) Roadmap
## v0.2.4 — Windows CLI Usability
## v0.2.6 — Rust Embedding API ✅ Completed
This release focuses on making Cassady feel reliable and native when the CLI is run on Windows. It covers runtime usability after `cass` or `cassady` is already available on the machine; installers, package managers, PATH setup, code signing, and update delivery are intentionally out of scope.
This release focuses on adding the first intentional Rust library surface for embedding Cassady in other Rust projects. The goal is to provide the bones for programmatic, headless agent sessions: configure a workspace, start or resume an agent session, send turns, stream typed events, and handle approvals without launching the TUI. See `plans/V0_2_6_RUST_EMBEDDING_API_PLAN.md`.
### Terminal Experience
### Experimental Public API
- [ ] **Make interactive rendering robust in Windows terminals.** Ensure chat, setup, confirmation prompts, streamed output, spinners, diffs, and tool summaries render cleanly in Windows Terminal, PowerShell, Command Prompt, and common VS Code integrated terminals.
- Enable or gracefully detect ANSI/VT support instead of emitting broken escape sequences.
- Respect `NO_COLOR`, non-interactive output, redirected stdout/stderr, and narrow terminal widths.
- Avoid relying on glyphs, emoji, box drawing, or cursor control sequences that render poorly on default Windows fonts.
- Keep wrapping and cursor positioning correct for multi-line input, Markdown output, and long tool-call summaries.
- [x] **Add a supported embedding module.** Provide a small `cassady::embedding` API with builder, session, turn, event, approval, and error types so callers do not need to stitch together internal modules directly.
- Mark the API experimental for v0.2.6 rather than promising long-term semver stability.
- Keep existing CLI/TUI behavior unchanged while steering library users toward the new module.
- [ ] **Harden keyboard handling on Windows.** Make the TUI and prompts respond predictably to Windows console input events.
- Verify `Enter`, `Backspace`, `Delete`, arrow keys, `Home`, `End`, `PageUp`, `PageDown`, `Tab`, and paste behavior.
- Preserve existing `Ctrl-C` cancellation semantics and handle `Ctrl-Break`/console close events gracefully where supported.
- Ensure `Esc` cancellation and prompt dismissal work consistently across PowerShell, Command Prompt, and Windows Terminal.
- [x] **Support host-configured agent sessions.** Let Rust callers create or resume headless sessions with explicit cwd, access mode, model/provider overrides, reasoning effort, and Cassady config root.
- Reuse existing config files, global instructions, bundled docs, security policy, and JSONL conversation storage.
- Avoid requiring callers to construct CLI-specific types.
- [ ] **Improve plain CLI output for Windows users.** Commands such as `cass check`, setup diagnostics, validation errors, and usage text should remain readable without a fully interactive terminal.
- Prefer actionable Windows examples using PowerShell syntax when the current platform is Windows.
- Avoid POSIX-only command snippets in runtime guidance unless explicitly labeled.
- Keep error messages copy/paste-friendly and free of terminal control characters when output is redirected.
### Headless Turn Execution
### Windows Paths and Files
- [x] **Run agent turns programmatically.** Add a Tokio-native API for sending one user message, streaming assistant/tool/status events, and returning the updated session or conversation state.
- Prevent or clearly reject overlapping turns unless the type design makes them impossible.
- Preserve provider streaming, tool execution, prompt generation, and context behavior from the existing agent loop.
- [ ] **Support Windows path syntax everywhere the CLI accepts paths.** Normalize and validate paths consistently across arguments, tool calls, diffs, session metadata, and model-visible file references.
- Handle drive-letter paths such as `C:\Users\name\project`, rooted paths such as `\temp`, UNC paths such as `\\server\share\repo`, and mixed `/`/`\` separators.
- Preserve user-facing paths in a readable Windows form while using canonicalized paths for safety decisions.
- Avoid treating `:` in drive letters as URL schemes or command separators.
- Add tests for relative path resolution from Windows workspaces and for paths containing spaces, apostrophes, parentheses, brackets, and non-ASCII characters.
- [x] **Expose approval handling to host applications.** Allow embedded callers to approve or deny tool approval requests, especially shell commands in `workspace-edit` mode.
- Include request id, tool call id, tool name, arguments, and reason in approval events.
- Document cancellation/drop behavior for active turns.
- [ ] **Respect Windows filesystem semantics in workspace policy.** Keep read, write, edit, and shell safety checks correct on NTFS and common Windows filesystems.
- Account for case-insensitive path comparisons, symlinks, junctions, directory symlinks, and network shares.
- Prevent workspace escapes through `..`, junctions, symlink targets, alternate path spellings, and UNC aliases.
- Handle reserved device names, trailing dots/spaces, invalid filename characters, and long-path edge cases with clear errors.
- Preserve current access modes (`read-only`, `workspace-edit`, `full-access`) with Windows-specific authorization tests.
### Documentation and Validation
- [ ] **Handle line endings and encodings cleanly.** Make file reads, edits, diffs, and generated files predictable on Windows projects.
- Preserve existing CRLF/LF style when editing files where practical.
- Render diffs clearly even when files use CRLF line endings.
- Avoid corrupting UTF-8 with BOM, UTF-16, or non-UTF-8 files; detect unsupported text encodings and explain the limitation.
- Keep binary-file detection reliable for Windows executables, images, archives, and generated build artifacts.
- [x] **Add a minimal headless example.** Include a compilable Rust example that imports Cassady, starts a session, sends a prompt, and prints streamed assistant output.
- Note that a configured OpenAI-compatible provider and API key are still required.
- Show where to handle approval requests even if the first example defaults to `read-only`.
### Shell and Process Integration
- [x] **Document the experimental Rust API.** Add bundled docs and README links for setup requirements, basic usage, event handling, approvals, limitations, and current non-goals.
- Make clear that multi-agent orchestration, custom providers, custom tools, daemons, and stable plugin APIs are deferred.
- [ ] **Use the right shell behavior on Windows.** Make `shell` tool execution, approval prompts, command summaries, cancellation, and exit status reporting work with Windows process semantics.
- Prefer PowerShell-friendly examples and diagnostics while still supporting `cmd.exe`-style commands when users provide them.
- Quote paths with spaces safely and avoid POSIX-only escaping in Windows-generated commands.
- Surface the actual executable, working directory, exit code, stdout, and stderr in a way users can debug.
- Cancel long-running child processes cleanly, including process trees where possible.
- [x] **Test embedding without a terminal.** Add integration tests that use temporary config/conversation roots and mock provider responses to verify session creation, turn streaming, resume, approval flow, and access-mode behavior.
- Ensure `cargo test --locked --all-targets` covers the new public API and examples.
- [ ] **Normalize environment-variable handling.** Ensure provider API key checks, diagnostics, setup guidance, and spawned tools work with Windows environment conventions.
- Treat environment variable names consistently despite Windows case-insensitive lookup behavior.
- Show PowerShell examples such as `$env:OPENAI_API_KEY = "..."` for temporary values.
- Avoid relying on POSIX shell expansion, `export`, `$VAR`, or `~` in Windows-specific guidance.
## v0.2.4 — System Prompt Refinement
- [ ] **Support common Windows external commands and editors.** When Cassady suggests or launches helper commands, make the behavior compatible with typical Windows environments.
- Detect missing tools and explain alternatives rather than assuming Unix utilities are present.
- Avoid hard dependencies on `sh`, `bash`, `grep`, `sed`, `cat`, `less`, or `/tmp` during normal CLI operation.
- Respect configured editor/browser commands and quote file paths correctly when opening files or URLs.
This release focuses on making Cassady's system prompt clearer, more intuitive, and more useful for everyday coding work without letting it become bulky. The target is a well-structured prompt around 1,000 tokens that gives the model enough product context, safety expectations, and workflow guidance to behave consistently across read-only, workspace-edit, and full-access sessions. See `plans/V0_2_4_SYSTEM_PROMPT_REFINEMENT_PLAN.md`.
### Config, State, and Session Usability
### Prompt Structure and Content
- [ ] **Use Windows-appropriate runtime locations.** Keep config, logs, caches, sessions, temporary files, and diagnostics in locations that align with Windows conventions.
- Prefer the existing cross-platform directory abstraction where available, and verify behavior with `APPDATA`, `LOCALAPPDATA`, `TEMP`, and `USERPROFILE`.
- Expand `~` and environment-derived paths consistently in config values.
- Keep session history portable enough to display Windows paths without breaking transcript replay.
- [x] **Restructure the prompt into clear sections.** Replace the current compact prompt with a polished, scannable structure that explains identity, operating principles, tool use, editing, safety, and response style in a predictable order.
- Keep headings short and model-friendly so the prompt is easy to follow during long sessions.
- Preserve the existing split between the reusable base prompt, user global instructions, and runtime constraints.
- Avoid duplicating long documentation that already lives in README or bundled docs.
- [ ] **Make diagnostics expose Windows-specific context.** Improve `cass check` and error reports so Windows users can understand terminal, filesystem, shell, and config problems quickly.
- Include OS, architecture, terminal detection, active shell, config path, workspace path, and access mode when relevant.
- Clearly distinguish provider/API-key failures from Windows runtime issues.
- Recommend Windows-native remediation steps without mentioning installation tasks.
- [x] **Add enough product context for intuitive behavior.** Teach the model what Cassady is, how the terminal chat works, and what the user can see without over-explaining implementation details.
- Explain that tool calls, tool results, diffs, approvals, and streamed assistant text are visible in the transcript.
- Clarify that Cassady is a coding assistant for real project work, so it should inspect before changing files, make targeted edits, and summarize outcomes honestly.
- Include guidance for asking focused follow-up questions only when necessary, instead of over-planning or guessing.
- [ ] **Keep aliases and command parsing consistent.** Ensure `cass` and `cassady` subcommands, flags, config overrides, and path arguments behave the same on Windows as on Unix-like systems.
- Validate quoting behavior for arguments containing spaces and backslashes.
- Ensure help text and examples do not imply shell features unavailable in PowerShell or Command Prompt.
- Keep machine-readable output stable across platforms when output is consumed by scripts.
- [x] **Keep the prompt intentionally compact.** Aim for roughly 900-1,100 tokens for the normal effective system prompt, including runtime access-mode guidance but excluding user-provided global instructions.
- Prefer dense, high-signal instructions over broad lists of examples.
- Remove redundant wording when new guidance overlaps with existing safety or response rules.
- Add a lightweight test or snapshot check so future prompt changes do not accidentally grow far beyond the intended size.
### Verification and Documentation
### Tool, Editing, and Safety Guidance
- [ ] **Add Windows-focused automated coverage.** Add unit and integration tests that exercise Windows path parsing, policy checks, config discovery, line endings, environment variables, and command rendering.
- Use platform-gated tests for behavior that can only run on Windows.
- Add platform-independent tests for Windows path strings where possible.
- Include regression tests for spaces in paths, UNC paths, CRLF edits, and workspace escape attempts.
- [x] **Improve tool-use instructions.** Make the prompt explicit about when to read, grep, edit, write, and shell while still letting the model choose the right tool for the task.
- Encourage targeted inspection before edits and `grep`/search before opening large or unknown files.
- Explain that the model should request tools directly when useful; Cassady will enforce access policy, denials, and approval prompts at runtime.
- Remind the model not to claim a tool succeeded until the tool result confirms it.
- [ ] **Run a manual Windows CLI acceptance pass.** Validate the release on a real Windows environment, not just cross-compilation.
- Test PowerShell, Command Prompt, Windows Terminal, and VS Code integrated terminal.
- Exercise interactive chat, first-run setup, `cass check`, tool approvals, file read/edit/diff, shell cancellation, and redirected output.
- Record any unsupported terminal or shell behavior as explicit known limitations.
- [x] **Sharpen editing guidance.** Make file-change behavior safer and more reliable, especially for exact-text edits.
- Prefer `edit` for focused modifications and `write` only for new files or intentional full rewrites.
- Instruct the model to keep replacements minimal, unique, and non-overlapping.
- Encourage running or suggesting relevant tests after meaningful code changes.
- [ ] **Update runtime documentation for Windows usage.** Refresh README and bundled docs with Windows-specific CLI usage guidance while avoiding installation instructions.
- Document PowerShell environment-variable examples, path examples, terminal expectations, and known limitations.
- Include troubleshooting for broken colors, bad wrapping, path authorization failures, CRLF diffs, and missing Unix helper commands.
- Keep all Windows guidance consistent with existing access modes and safety policies.
- [x] **Make access-mode behavior easy for the model to follow.** Rewrite read-only, workspace-edit, and full-access guidance in plain language that maps directly to available tools.
- Keep workspace and bundled-doc boundaries clear.
- State that shell approval is handled by Cassady's UI rather than by asking for permission in chat.
- Preserve conservative behavior when a task requires permissions the current mode does not allow.
## v0.2.3 — Documentation and README Refresh
### Validation and Documentation
This release focuses on making Cassady understandable, trustworthy, and easy to operate by rewriting the README and bringing all bundled documentation up to date with the current CLI behavior. The work should cover user-facing documentation only; broad CLI feature work and Windows-specific runtime improvements are deferred to v0.2.4. See `plans/V0_2_3_DOCUMENTATION_README_REFRESH_PLAN.md`.
- [x] **Add prompt-focused tests.** Verify that the generated prompt includes the required sections, preserves global instructions, reflects the active access mode, and stays within the intended size range.
- Cover read-only, workspace-edit, and full-access effective prompts.
- Include a regression check for prompt ordering so runtime constraints remain near the end.
- [x] **Update user-facing references to global instructions.** Refresh docs only where needed to explain how `~/.cass/global.md` fits into the structured prompt.
- Avoid exposing the full internal prompt in documentation.
- Mention that user global instructions are respected unless they conflict with runtime safety constraints.
## v0.2.3 — Documentation and README Refresh ✅ Completed
This release focuses on making Cassady understandable, trustworthy, and easy to operate by rewriting the README and bringing all bundled documentation up to date with the current CLI behavior. The work should cover user-facing documentation only; broad CLI feature work and Windows-specific runtime improvements are deferred to the planned Windows CLI usability work. See `plans/V0_2_3_DOCUMENTATION_README_REFRESH_PLAN.md`.
### README Rewrite
@@ -156,9 +146,9 @@ This release focuses on making Cassady understandable, trustworthy, and easy to
- Updating config or switching providers/models.
- Resuming work after a failed provider request or cancelled turn.
- [x] **Document platform expectations without duplicating future Windows work.** Add accurate notes for macOS, Linux, and Windows users while keeping deep Windows CLI usability improvements scoped to v0.2.4.
- [x] **Document platform expectations without duplicating future Windows work.** Add accurate notes for macOS, Linux, and Windows users while keeping deep Windows CLI usability improvements scoped to the planned Windows CLI usability work.
- Include path, shell, and environment-variable examples for each platform when documentation needs them.
- Mark known Windows limitations clearly until the v0.2.4 work lands.
- Mark known Windows limitations clearly until the planned Windows CLI usability work lands.
- Avoid promising installer, package manager, or auto-update behavior that is not implemented.
### Documentation Quality and Maintenance
@@ -274,3 +264,101 @@ This release focuses on making Cass easier to interrupt, easier to audit, and sa
- [x] **Edit diff output.** Make `edit` changes reviewable in the transcript.
- First version: show a unified before/after diff after the edit is applied.
- Later versions may add pre-apply approval, but that requires a confirmation flow between tools and the TUI.
## Planned within the next major release
These sections describe work Cassady intends to complete before or as part of the next major release, but which has not yet been assigned to a specific version. Scope, order, and version numbers may change.
### Windows CLI Usability
This work focuses on making Cassady feel reliable and native when the CLI is run on Windows. It covers runtime usability after `cass` or `cassady` is already available on the machine; installers, package managers, PATH setup, code signing, and update delivery are intentionally out of scope.
#### Terminal Experience
- [ ] **Make interactive rendering robust in Windows terminals.** Ensure chat, setup, confirmation prompts, streamed output, spinners, diffs, and tool summaries render cleanly in Windows Terminal, PowerShell, Command Prompt, and common VS Code integrated terminals.
- Enable or gracefully detect ANSI/VT support instead of emitting broken escape sequences.
- Respect `NO_COLOR`, non-interactive output, redirected stdout/stderr, and narrow terminal widths.
- Avoid relying on glyphs, emoji, box drawing, or cursor control sequences that render poorly on default Windows fonts.
- Keep wrapping and cursor positioning correct for multi-line input, Markdown output, and long tool-call summaries.
- [ ] **Harden keyboard handling on Windows.** Make the TUI and prompts respond predictably to Windows console input events.
- Verify `Enter`, `Backspace`, `Delete`, arrow keys, `Home`, `End`, `PageUp`, `PageDown`, `Tab`, and paste behavior.
- Preserve existing `Ctrl-C` cancellation semantics and handle `Ctrl-Break`/console close events gracefully where supported.
- Ensure `Esc` cancellation and prompt dismissal work consistently across PowerShell, Command Prompt, and Windows Terminal.
- [ ] **Improve plain CLI output for Windows users.** Commands such as `cass check`, setup diagnostics, validation errors, and usage text should remain readable without a fully interactive terminal.
- Prefer actionable Windows examples using PowerShell syntax when the current platform is Windows.
- Avoid POSIX-only command snippets in runtime guidance unless explicitly labeled.
- Keep error messages copy/paste-friendly and free of terminal control characters when output is redirected.
#### Windows Paths and Files
- [ ] **Support Windows path syntax everywhere the CLI accepts paths.** Normalize and validate paths consistently across arguments, tool calls, diffs, session metadata, and model-visible file references.
- Handle drive-letter paths such as `C:\Users\name\project`, rooted paths such as `\temp`, UNC paths such as `\\server\share\repo`, and mixed `/`/`\` separators.
- Preserve user-facing paths in a readable Windows form while using canonicalized paths for safety decisions.
- Avoid treating `:` in drive letters as URL schemes or command separators.
- Add tests for relative path resolution from Windows workspaces and for paths containing spaces, apostrophes, parentheses, brackets, and non-ASCII characters.
- [ ] **Respect Windows filesystem semantics in workspace policy.** Keep read, write, edit, and shell safety checks correct on NTFS and common Windows filesystems.
- Account for case-insensitive path comparisons, symlinks, junctions, directory symlinks, and network shares.
- Prevent workspace escapes through `..`, junctions, symlink targets, alternate path spellings, and UNC aliases.
- Handle reserved device names, trailing dots/spaces, invalid filename characters, and long-path edge cases with clear errors.
- Preserve current access modes (`read-only`, `workspace-edit`, `full-access`) with Windows-specific authorization tests.
- [ ] **Handle line endings and encodings cleanly.** Make file reads, edits, diffs, and generated files predictable on Windows projects.
- Preserve existing CRLF/LF style when editing files where practical.
- Render diffs clearly even when files use CRLF line endings.
- Avoid corrupting UTF-8 with BOM, UTF-16, or non-UTF-8 files; detect unsupported text encodings and explain the limitation.
- Keep binary-file detection reliable for Windows executables, images, archives, and generated build artifacts.
#### Shell and Process Integration
- [ ] **Use the right shell behavior on Windows.** Make `shell` tool execution, approval prompts, command summaries, cancellation, and exit status reporting work with Windows process semantics.
- Prefer PowerShell-friendly examples and diagnostics while still supporting `cmd.exe`-style commands when users provide them.
- Quote paths with spaces safely and avoid POSIX-only escaping in Windows-generated commands.
- Surface the actual executable, working directory, exit code, stdout, and stderr in a way users can debug.
- Cancel long-running child processes cleanly, including process trees where possible.
- [ ] **Normalize environment-variable handling.** Ensure provider API key checks, diagnostics, setup guidance, and spawned tools work with Windows environment conventions.
- Treat environment variable names consistently despite Windows case-insensitive lookup behavior.
- Show PowerShell examples such as `$env:OPENAI_API_KEY = "..."` for temporary values.
- Avoid relying on POSIX shell expansion, `export`, `$VAR`, or `~` in Windows-specific guidance.
- [ ] **Support common Windows external commands and editors.** When Cassady suggests or launches helper commands, make the behavior compatible with typical Windows environments.
- Detect missing tools and explain alternatives rather than assuming Unix utilities are present.
- Avoid hard dependencies on `sh`, `bash`, `grep`, `sed`, `cat`, `less`, or `/tmp` during normal CLI operation.
- Respect configured editor/browser commands and quote file paths correctly when opening files or URLs.
#### Config, State, and Session Usability
- [ ] **Use Windows-appropriate runtime locations.** Keep config, logs, caches, sessions, temporary files, and diagnostics in locations that align with Windows conventions.
- Prefer the existing cross-platform directory abstraction where available, and verify behavior with `APPDATA`, `LOCALAPPDATA`, `TEMP`, and `USERPROFILE`.
- Expand `~` and environment-derived paths consistently in config values.
- Keep session history portable enough to display Windows paths without breaking transcript replay.
- [ ] **Make diagnostics expose Windows-specific context.** Improve `cass check` and error reports so Windows users can understand terminal, filesystem, shell, and config problems quickly.
- Include OS, architecture, terminal detection, active shell, config path, workspace path, and access mode when relevant.
- Clearly distinguish provider/API-key failures from Windows runtime issues.
- Recommend Windows-native remediation steps without mentioning installation tasks.
- [ ] **Keep aliases and command parsing consistent.** Ensure `cass` and `cassady` subcommands, flags, config overrides, and path arguments behave the same on Windows as on Unix-like systems.
- Validate quoting behavior for arguments containing spaces and backslashes.
- Ensure help text and examples do not imply shell features unavailable in PowerShell or Command Prompt.
- Keep machine-readable output stable across platforms when output is consumed by scripts.
#### Verification and Documentation
- [ ] **Add Windows-focused automated coverage.** Add unit and integration tests that exercise Windows path parsing, policy checks, config discovery, line endings, environment variables, and command rendering.
- Use platform-gated tests for behavior that can only run on Windows.
- Add platform-independent tests for Windows path strings where possible.
- Include regression tests for spaces in paths, UNC paths, CRLF edits, and workspace escape attempts.
- [ ] **Run a manual Windows CLI acceptance pass.** Validate the release on a real Windows environment, not just cross-compilation.
- Test PowerShell, Command Prompt, Windows Terminal, and VS Code integrated terminal.
- Exercise interactive chat, first-run setup, `cass check`, tool approvals, file read/edit/diff, shell cancellation, and redirected output.
- Record any unsupported terminal or shell behavior as explicit known limitations.
- [ ] **Update runtime documentation for Windows usage.** Refresh README and bundled docs with Windows-specific CLI usage guidance while avoiding installation instructions.
- Document PowerShell environment-variable examples, path examples, terminal expectations, and known limitations.
- Include troubleshooting for broken colors, bad wrapping, path authorization failures, CRLF diffs, and missing Unix helper commands.
- Keep all Windows guidance consistent with existing access modes and safety policies.
+64
View File
@@ -0,0 +1,64 @@
# Security Policy
## Supported Versions
Security updates are provided for the latest released version of Cassady. If you are using an older version, please upgrade to the latest release before reporting an issue, unless the issue also affects the latest release.
| Version | Supported |
| ------- | --------- |
| Latest | ✅ |
| Older releases | ❌ |
## Reporting a Vulnerability
Please do **not** report security vulnerabilities in public GitHub issues, discussions, or pull requests.
To report a vulnerability, use GitHub's private vulnerability reporting for this repository:
1. Open the repository on GitHub.
2. Go to **Security** → **Report a vulnerability**.
3. Include as much detail as you can about the issue, impact, affected versions, and steps to reproduce.
## What to Include
Helpful reports include:
- A description of the vulnerability and likely impact.
- Steps to reproduce or a minimal proof of concept.
- The Cassady version, operating system, shell, and terminal environment.
- Relevant configuration details with secrets removed.
- Any known mitigations or workarounds.
Do not include live API keys, tokens, private prompts, or sensitive project files in a report. Redact secrets before sharing logs or configuration.
## Response Expectations
After a report is received, the maintainer will aim to:
- Acknowledge the report within 7 days.
- Confirm whether the issue is in scope and reproducible.
- Provide status updates when there is meaningful progress.
- Coordinate disclosure timing before publishing details publicly.
Security fixes may be released as patch versions when appropriate. Public disclosure should wait until a fix or mitigation is available, unless otherwise coordinated with the maintainer.
## Scope
Examples of in-scope issues include vulnerabilities in Cassady that could:
- Bypass documented access modes, approval prompts, or workspace boundaries.
- Cause unintended file reads, writes, edits, or shell command execution.
- Leak API keys, provider credentials, chat history, or local configuration.
- Corrupt or expose session data stored under `~/.cass`.
- Introduce unsafe behavior in bundled release artifacts.
Out-of-scope issues generally include:
- Vulnerabilities in third-party model providers or APIs not controlled by this project.
- Prompt-injection behavior that does not bypass Cassady's documented safety controls.
- Issues that require a compromised local machine, shell, dependency cache, or provider account.
- Reports against unsupported older versions that are fixed in the latest release.
## Safe Harbor
Good-faith security research is welcome. Please avoid privacy violations, data destruction, service disruption, and accessing data that does not belong to you. If you follow this policy and make a good-faith effort to avoid harm, the maintainer will not pursue legal action for your research.
+1
View File
@@ -10,6 +10,7 @@ Cassady tools may list, search, and read this directory. Mutating tools are bloc
- [Configuration](configuration.md): `~/.cass` files, setup, precedence, schema examples, and validation.
- [Providers and models](providers.md): built-in OpenAI-compatible providers, custom endpoints, model discovery, and reasoning metadata.
- [Access modes and tool safety](access-modes.md): what tools can read, write, edit, and run in each mode.
- [Experimental Rust embedding API](embedding.md): import Cassady from Rust, start headless sessions, stream events, and handle approvals.
- [Workflows](workflows.md): common ways to inspect code, apply edits, run checks, switch models, and resume chats.
- [Troubleshooting](troubleshooting.md): symptoms, likely causes, fixes, and verification commands.
- [Platform notes](platforms.md): macOS, Linux, and Windows environment/path notes.
+1 -1
View File
@@ -5,7 +5,7 @@ Cassady reads user-editable config files from `~/.cass`.
- `config.json`: user preferences, active defaults, and compatibility fields.
- `providers.json`: provider connection definitions.
- `models.json`: model metadata.
- `global.md`: optional global instructions included in new chats.
- `global.md`: optional global instructions included in new chat system prompts when they fit the active request; they cannot override access modes, tool denials, approvals, or workspace boundaries.
- `conversations/`: saved JSONL chats.
- `docs/`: bundled docs installed from the current binary.
+98
View File
@@ -0,0 +1,98 @@
# Experimental Rust embedding API
Cassady v0.2.6 includes an experimental Rust API for running headless agent sessions from another Rust program. The API is intended for early integrations and may change before Cassady declares a stable library contract.
The embedding API uses the same provider configuration, global instructions, prompts, access modes, tools, and JSONL conversation storage as the `cass` terminal UI. By default it reads and writes under `~/.cass`, so run `cass setup` first or create compatible `config.json`, `providers.json`, and `models.json` files programmatically.
## Minimal example
```rust
use cassady::prelude::*;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let session = SessionBuilder::new()
.cwd(std::env::current_dir()?)
.access_mode(AccessMode::ReadOnly)
.build()
.await?;
let mut turn = session
.start_turn("Summarize this project in a few sentences.")
.await?;
while let Some(event) = turn.next_event().await? {
match event {
Event::AssistantChunk(text) => print!("{text}"),
Event::Finished => break,
_ => {}
}
}
let session = turn.finish().await?;
eprintln!("\nResume chat with: cass --resume {}", session.id());
Ok(())
}
```
Add Cassady from a git checkout or path dependency, and ensure your application runs on Tokio.
## Creating or resuming sessions
Use `SessionBuilder` to set host-controlled options:
```rust
let session = SessionBuilder::new()
.config_root("/tmp/my-cass-root")
.cwd("/path/to/workspace")
.access_mode(AccessMode::WorkspaceEdit)
.model("my-model")
.base_url("https://provider.example/v1")
.api_key_env("MY_PROVIDER_KEY")
.build()
.await?;
let resumed = SessionBuilder::new()
.cwd("/path/to/workspace")
.resume(session.id())
.await?;
```
`build()` is equivalent to `new_session()`. Resumed and new sessions use Cassady's normal `conversations/*.jsonl` files, so CLI and embedded sessions can interoperate.
## Events and approvals
`Session::start_turn` consumes the session and returns a `Turn`. This type design prevents overlapping turns for the same session. Call `turn.finish().await?` after receiving `Event::Finished` to recover the updated `Session`.
Important events include:
- `AssistantChunk` and `ReasoningChunk`
- `ToolCallStarted`, `ToolOutputChunk`, and `ToolResult`
- `ApprovalRequested` and `ApprovalResolved`
- `Status`
- `Finished`
When a tool needs approval, decide in host code:
```rust
while let Some(event) = turn.next_event().await? {
match event {
Event::ApprovalRequested(request) => {
eprintln!("approval needed for {}: {}", request.name, request.reason);
turn.deny(&request.request_id)?;
}
Event::Finished => break,
_ => {}
}
}
```
The approval policy is the same as the TUI: shell is unavailable in `read-only`, requires approval in `workspace-edit`, and runs directly in `full-access` unless destructive-operation confirmation is enabled.
## Cancellation
Dropping a `Turn` aborts the underlying task. Prefer `turn.cancel().await?` when you want Cassady to repair the conversation with cancellation records before returning the session.
## Current limitations
The v0.2.6 API is intentionally small and experimental. It does not include custom provider traits, custom tools, plugin loading, multi-agent orchestration, background daemons, task queues, or a synchronous/blocking wrapper.
+1 -1
View File
@@ -14,7 +14,7 @@
**Exact edit**: An `edit` tool replacement where each `old_text` must match exactly once in the original file before anything is written.
**Global instructions**: Optional text in `~/.cass/global.md` included in new chat system prompts.
**Global instructions**: Optional text in `~/.cass/global.md` included in new chat system prompts. Cassady follows these instructions when they fit the active request, but they cannot override runtime safety constraints such as access modes, tool denials, approvals, or workspace boundaries.
**Model metadata**: The `models.json` entry describing a model id, owning provider, display name, context limits, tool/streaming support, and reasoning behavior.
+33
View File
@@ -0,0 +1,33 @@
use cassady::prelude::*;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let session = SessionBuilder::new()
.cwd(std::env::current_dir()?)
.access_mode(AccessMode::ReadOnly)
.build()
.await?;
let mut turn = session
.start_turn("Summarize this project in a few sentences.")
.await?;
while let Some(event) = turn.next_event().await? {
match event {
Event::AssistantChunk(text) => print!("{text}"),
Event::ApprovalRequested(request) => {
eprintln!(
"approval requested for {}: {}; denying in this example",
request.name, request.reason
);
turn.deny(&request.request_id)?;
}
Event::Finished => break,
_ => {}
}
}
let session = turn.finish().await?;
eprintln!("\nResume chat with: cass --resume {}", session.id());
Ok(())
}
@@ -0,0 +1,499 @@
# v0.2.4 System Prompt Refinement Implementation Plan
## Goal
v0.2.4 refines Cassady's generated system prompt so the model receives clearer, more intuitive operating instructions without turning the prompt into a long manual. The effective prompt should explain Cassady's role, terminal transcript behavior, tool use, editing expectations, runtime safety constraints, and response style in a structured way that models can follow reliably.
Success statement:
> A normal effective system prompt, excluding user-provided global instructions, is roughly 900-1,100 tokens and consistently guides the model to inspect before editing, use tools directly when useful, respect access modes, make targeted file changes, and finish each turn with an honest concise response.
## Scope
### In scope
- Rewrite `src/prompt.rs` prompt text into a clearer sectioned structure.
- Preserve the existing prompt-generation model:
- `build_base_system_prompt(global)` creates reusable conversation-level instructions.
- `build_effective_system_prompt(...)` appends model/workspace/docs/access/tool runtime constraints.
- `~/.cass/global.md` text remains embedded as user global instructions when present.
- Add product context that helps the model understand Cassady's terminal chat UX.
- Improve guidance for tool selection, exact-text edits, use of shell, and test/summarization behavior.
- Make access-mode guidance concise and easy to map to the currently allowed tools.
- Add focused tests for prompt sections, ordering, global instructions, access modes, and approximate size.
- Update docs only where they mention global instructions or prompt behavior.
- Keep the prompt provider-agnostic and compatible with all OpenAI-compatible models Cassady supports.
### Out of scope
- Adding prompt templates, profile selection, or user-selectable prompt modes.
- Exposing a CLI command to print or edit the full generated system prompt.
- Changing the `~/.cass/global.md` file format or adding layered project instructions.
- Changing access-policy enforcement, tool schemas, approval UI, or security decisions.
- Adding new tools or changing tool argument formats.
- Implementing automatic prompt compression or conversation summarization.
- Maintaining separate prompts per provider/model family.
- Stuffing large reference documentation, provider catalogs, or CLI help into the system prompt.
## Context and Current State
Relevant files:
- `src/prompt.rs`: builds both the base and effective system prompts. The current prompt is short and functional but sparse.
- `src/app.rs`: reads global instructions and stores the base prompt in new conversations.
- `src/agent.rs`: calls `build_effective_system_prompt(...)` before provider requests.
- `src/conversation.rs`: persists the base system prompt in the conversation record and reuses it when a chat is resumed.
- `src/access.rs`: defines `read-only`, `workspace-edit`, and `full-access` modes.
- `src/security.rs`: central policy for tool availability, read/write boundaries, shell approval, and denials.
- `src/tools/*`: tool implementations and schemas for `ls`, `read`, `grep`, `write`, `edit`, and `shell`.
- `docs/glossary.md`: defines global instructions as optional text in `~/.cass/global.md` included in new chat system prompts.
- `tests/*`: no dedicated prompt tests exist yet; prompt behavior is only indirectly covered through agent/conversation tests.
Current prompt behavior to preserve:
- Cassady identifies itself as Cassady/Cass, a coding agent running in a terminal chat interface.
- User global instructions are included only when non-empty after trimming.
- Global instructions are subordinate to runtime safety constraints.
- The effective prompt includes:
- model id,
- active access mode,
- launch working directory,
- bundled docs directory,
- allowed tools,
- access-mode-specific instructions,
- final response behavior.
- Tool access is ultimately enforced by runtime policy, not by prompt wording alone.
Current gaps:
- The prompt is organized as numbered sections but does not fully explain Cassady's user-visible transcript model.
- Tool and editing guidance is too compact for models that need stronger direction on when to inspect, search, edit, write, or run shell.
- Access-mode text is accurate but can be made more direct and less repetitive.
- There is no automated check that future prompt edits preserve required sections or stay near the intended size.
- Documentation mentions global instructions, but not how they relate to runtime safety constraints in the refined prompt.
## Design Principles
1. **High signal, low bulk.** The prompt should contain the instructions most likely to improve model behavior, not a copy of the README.
2. **Runtime policy remains authoritative.** Prompt text should guide the model, while `src/security.rs` and tool availability continue to enforce real permissions.
3. **Structure beats length.** Use clear headings and dense paragraphs/bullets so models can find instructions during long sessions.
4. **Tell the model what the user can see.** Explain streamed output, visible tool calls/results, approvals, and edit diffs so the assistant does not narrate inaccurately.
5. **Prefer action over ceremony.** Encourage the model to use tools directly, inspect before changing files, and ask questions only when missing information materially blocks progress.
6. **Make editing rules concrete.** Exact-text edits are a core reliability constraint and should be stated plainly.
7. **Avoid provider-specific assumptions.** The prompt should work for small and large OpenAI-compatible models without relying on special model behavior.
8. **Keep user instructions safe.** Global instructions are important, but they must never override runtime access modes, tool denials, or user requests in the active chat.
## Prompt Architecture
Keep the two-stage prompt generation, but make the internal structure more intentional.
### Base prompt
`build_base_system_prompt(global)` should contain stable instructions that are true for every session:
1. Identity and role.
2. Operating principles.
3. User global instructions, when present.
4. Tool-use behavior.
5. Editing behavior.
6. Response behavior.
The base prompt is stored in the conversation when a chat is created. Because resumed chats reuse the stored base prompt, changing the base prompt affects new chats but not necessarily existing conversations. That behavior is acceptable and should be documented only if user-facing docs mention prompt changes.
### Effective prompt
`build_effective_system_prompt(...)` should append runtime-specific information near the end:
1. Current runtime context:
- model,
- access mode,
- launch working directory,
- bundled docs directory,
- allowed tools.
2. Access-mode rules for the active mode.
3. Final reminder that runtime policy and tool results are authoritative.
Runtime constraints should remain near the end so they are fresh in the model's context and can override earlier general instructions.
### Numbering and headings
Use stable Markdown-like headings rather than fragile sentence-only text. For example:
```text
# Cassady operating instructions
## Role
...
## Working style
...
```
Numbered headings are acceptable if tests are written against section names rather than exact numbers. Avoid deeply nested outlines.
## Target Prompt Content
The final wording can change during implementation, but it should cover the following content.
### 1. Role
Required ideas:
- You are Cassady, also called Cass.
- You are a coding assistant inside an interactive terminal chat.
- You help with real project work: reading code, explaining behavior, editing files, and running relevant commands when allowed.
- Work carefully and honestly; do not pretend to have inspected or changed files unless tool results confirm it.
Avoid:
- Overly broad claims such as being a general-purpose autonomous system.
- Long branding language.
- Any implication that prompt instructions can bypass runtime access policy.
### 2. Working style
Required ideas:
- Prefer concrete progress over long speculative plans.
- Inspect relevant files before making claims or edits.
- Ask a focused follow-up question only when the task is ambiguous or blocked.
- Keep explanations concise but include enough context for the user to review the work.
- If the user's request is impossible in the current mode, explain the limitation and the next viable step.
Suggested wording style:
```text
Make the smallest useful plan, then act. Do not over-plan routine code tasks. When information is missing, gather it with tools if possible; ask the user only when a choice or secret is genuinely required.
```
### 3. Transcript and UI awareness
Required ideas:
- Assistant text is streamed to the user.
- Tool calls and tool results are visible in the transcript.
- Edit diffs and approval prompts may be shown by Cassady's UI.
- The model should request tools directly rather than asking for chat permission before every tool call.
- Cassady handles access denials and approval UI separately.
This section should reduce behaviors such as:
- Saying "I will run X" and then not calling the tool.
- Asking "May I read the file?" when the tool is available.
- Claiming a shell command ran before its result arrives.
- Repeating huge summaries of tool output that the user can already see.
### 4. Tool use
Required guidance by tool area:
- `ls`: use for directory orientation.
- `grep`: use before reading large or unknown files, or to locate definitions/usages.
- `read`: use targeted reads for files or ranges that matter.
- `edit`: use for focused changes to existing files.
- `write`: use for new files or intentional full rewrites.
- `shell`: use for tests, builds, formatting, diagnostics, or project commands when allowed and useful.
General instructions:
- Use tools when current filesystem state matters.
- Prefer targeted inspection over guessing.
- Batch related reads when possible, but avoid reading unrelated files.
- Do not use shell for file inspection when `ls`/`grep`/`read` is safer and sufficient.
- If a tool is denied, adapt to the denial instead of repeating the same call.
### 5. Editing
Required ideas:
- Prefer `edit` for small and medium modifications to existing files.
- `edit` replacements must use exact old text that appears uniquely in the original file.
- Keep edits minimal, unique, and non-overlapping.
- Combine related replacements for the same file in one `edit` call when practical.
- Use `write` only for new files or full rewrites where that is safer and intentional.
- After meaningful code changes, run relevant tests/formatters when allowed, or tell the user what should be run.
- Mention changed files and verification in the final response.
### 6. Safety and access modes
The base prompt should state the general principle:
- Follow runtime constraints and active access mode.
- Do not try to bypass workspace boundaries, docs read-only rules, approvals, or tool denials.
The effective prompt should include active-mode-specific guidance:
#### read-only
- Allowed tools should normally be `ls`, `read`, and `grep`.
- Inspect only inside the launch workspace and bundled docs directory.
- Do not request `write`, `edit`, or `shell`.
- If changes or commands are needed, explain that a more permissive access mode is required.
#### workspace-edit
- Read/list/search inside the launch workspace and bundled docs directory.
- Write/edit only inside the launch workspace.
- Bundled docs are read-only.
- Shell may be requested when useful, but Cassady will show the approval UI; do not ask for shell permission in chat first.
- If a path escapes the workspace, choose an in-workspace alternative or explain the limitation.
#### full-access
- `ls`, `read`, `grep`, `write`, `edit`, and `shell` may be requested when needed.
- Shell runs from the launch working directory.
- Normal OS permissions still apply.
- Bundled docs remain read-only for write/edit.
- Even in full-access, keep changes targeted and avoid destructive commands unless the user explicitly requested them and the action is necessary.
### 7. Final response behavior
Required ideas:
- Always end the turn with a concise user-facing response after tool work.
- Do not finish with only tool calls.
- Summarize what changed, where, and how it was verified.
- If no changes were made, summarize findings or blockers.
- Be honest about failures, denials, skipped tests, or assumptions.
## Approximate Prompt Budget
Target size: roughly 900-1,100 tokens for the normal effective system prompt, excluding user global instructions.
Because Cassady does not currently include a tokenizer, implement a simple approximate check rather than adding a heavy tokenizer dependency unless the implementer strongly prefers otherwise.
Recommended helper for tests:
```rust
fn approximate_token_count(s: &str) -> usize {
s.split_whitespace().count() * 4 / 3
}
```
This heuristic is intentionally rough. The test should prevent accidental prompt bloat, not enforce an exact model-token count. Suggested limits:
- Base prompt without global instructions: approximately 650-850 heuristic tokens.
- Effective prompt in each access mode without global instructions: approximately 900-1,150 heuristic tokens.
If the final prompt is slightly outside the target but demonstrably better, prefer readability over gaming the heuristic. The acceptance target should remain "around 1,000 tokens," not an exact failure-prone threshold.
## Proposed Prompt Skeleton
This skeleton is illustrative, not a required exact implementation.
```text
# Cassady operating instructions
## Role
You are Cassady, also called Cass, a coding assistant running in an interactive terminal chat. Help with real project work: inspect files, explain code, make targeted edits, and run useful commands when allowed. Work carefully and do not claim that files were read, commands ran, or edits succeeded until tool results confirm it.
## Working style
Make the smallest useful plan, then act. Prefer current project evidence over guesses. Use tools to gather missing filesystem context. Ask a focused follow-up question only when a user choice, secret, or missing requirement blocks progress. Keep user-facing explanations concise and practical.
## Transcript and tools
Assistant text is streamed. Tool calls, tool results, approvals, and edit diffs are visible in the transcript. Request tools directly when they are the right next step; Cassady enforces access policy and shows approval prompts separately. If a tool is denied or fails, adapt and explain the limitation.
## Tool use
Use ls for directory orientation, grep to locate text or inspect large/unknown areas, read for relevant files or ranges, edit for targeted changes, write for new files or intentional full rewrites, and shell for tests/builds/diagnostics when allowed. Prefer targeted reads and related batched reads over broad exploration.
## Editing
Inspect before editing. For edit, each old text must match exactly and uniquely in the original file; keep replacements minimal and non-overlapping. Do not use write for small changes to existing files. After meaningful code changes, run relevant verification when allowed or state what should be run.
## User global instructions
...
## Runtime context
Model: ...
Access mode: ...
Launch working directory: ...
Bundled Cass docs directory: ...
Allowed tools this turn: ...
## Access rules for this session
...
## Final response
End every turn with a concise response. Summarize changed files and verification, or summarize findings/blockers if no change was made. Do not end with only tool calls.
```
## Implementation Steps
### 1. Inventory exact current behavior
- Review `src/prompt.rs`, `src/agent.rs`, `src/app.rs`, `src/conversation.rs`, `src/access.rs`, `src/security.rs`, and `src/tools/schema.rs`.
- Confirm current tool names and per-mode availability from `SecurityPolicy::tool_availability`.
- Confirm docs directory behavior and blocked write roots from app/tool context construction.
- Confirm how global instructions are loaded and trimmed.
- Confirm how resumed conversations reuse the stored base prompt.
### 2. Rewrite `build_base_system_prompt`
- Replace the current compact numbered prompt with structured, high-signal sections.
- Include identity, working style, transcript/tool visibility, general tool guidance, editing guidance, global instructions, and response behavior.
- Preserve trimming behavior for `global`.
- Keep global instructions clearly labelled and explicitly subordinate to runtime safety constraints.
- Avoid including runtime-only values in the base prompt.
### 3. Rewrite `build_effective_system_prompt`
- Keep appending to `base.trim_end()`.
- Add a clear `Runtime context` section with model, access mode, cwd, docs dir, and allowed tools.
- Add one active-mode-specific `Access rules for this session` paragraph/bullet set.
- Keep runtime constraints after global/base text.
- Keep the final response reminder either at the end of the base prompt or at the end of the effective prompt. If it remains in the base prompt, add a short final runtime-policy reminder after access rules.
### 4. Add prompt tests
Create `tests/prompt_tests.rs` or add focused unit tests in `src/prompt.rs`. Prefer integration tests in `tests/prompt_tests.rs` so prompt behavior is covered through the public crate API if exports allow it.
Recommended tests:
1. **Base prompt has required sections.**
- Build with `None`.
- Assert it contains headings/phrases for role, working style, tools, editing, and final response behavior.
- Assert it does not contain runtime-only paths or model labels.
2. **Global instructions are included and trimmed.**
- Build with whitespace-wrapped global text.
- Assert the exact trimmed content appears.
- Assert the prompt says global instructions cannot conflict with runtime safety constraints.
3. **Empty global instructions are omitted.**
- Build with `Some(" \n")`.
- Assert the global-instructions heading is absent.
4. **Effective prompt includes runtime context.**
- Use temporary or fixed paths for cwd/docs.
- Assert model, mode, cwd, docs dir, and allowed tools are present.
5. **Each access mode gets correct instructions.**
- `read-only`: contains no write/edit/shell request guidance and says more permissive mode is needed for modifications.
- `workspace-edit`: says write/edit only inside workspace and shell approval is handled by Cassady UI.
- `full-access`: says all tools may be requested, shell runs from cwd, docs remain read-only.
6. **Runtime constraints stay after global instructions.**
- Build base with global text, then effective prompt.
- Assert global text index is before runtime context index.
- Assert access rules appear after runtime context.
7. **Prompt size remains intentional.**
- Build effective prompts for all modes without global instructions.
- Use the approximate token helper.
- Assert each is within the chosen guardrail, for example `800..=1250` approximate tokens.
8. **Allowed tools list reflects caller input.**
- Pass a small custom tool list.
- Assert the rendered list matches it.
Test guidance:
- Avoid asserting the entire prompt as one giant snapshot unless the project already uses snapshot testing.
- Prefer stable phrases and section headings so minor copy edits do not make tests brittle.
- If a snapshot is added, keep it intentionally small or use one golden prompt plus semantic tests.
### 5. Update docs references
Update only docs that need to mention global instructions or prompt behavior.
Likely files:
- `docs/glossary.md`: expand `Global instructions` to say they are included in new chat system prompts and followed unless they conflict with runtime safety constraints.
- `docs/configuration.md` or `README.md` only if they already mention `~/.cass/global.md` and need clarification.
Do not publish the full internal system prompt in docs. It is implementation detail and will evolve.
### 6. Run verification
Required commands:
```sh
cargo fmt --check
cargo test --locked --all-targets
```
If prompt tests use temp paths or platform-dependent path display, run on the current platform and avoid hardcoding separators where possible.
## Tests
Automated tests to add:
- New prompt tests covering base prompt structure, global instruction behavior, effective runtime context, access-mode-specific wording, ordering, and approximate size.
- Existing agent/conversation/tool tests should continue to pass unchanged.
Manual checks:
- Read one generated prompt for each access mode and verify it is understandable as prose.
- Check that the prompt does not repeat the same instruction in several sections.
- Check that docs/global-instruction wording matches the generated prompt.
- Confirm a normal prompt is around 1,000 tokens by the chosen heuristic or an external tokenizer if one is available.
## Documentation
Required documentation updates are intentionally small:
- `docs/glossary.md`: update the `Global instructions` definition.
- Any existing README/configuration references to `~/.cass/global.md`: clarify that these instructions are included in new chat system prompts and cannot override safety constraints.
No new user guide is required for this release unless implementation adds user-visible commands or configuration, which is out of scope.
## Compatibility and Migration Notes
- Existing conversations keep the base system prompt stored when they were created. The refined base prompt will apply to new conversations.
- Runtime constraints are still generated at request time, so active access mode, cwd, docs dir, and allowed tools remain current for resumed chats.
- `~/.cass/global.md` remains plain text and does not require migration.
- No config schema changes are expected.
- No provider/model configuration changes are expected.
## Risks and Mitigations
### Risk: prompt grows too large
Mitigation:
- Add a size guardrail test.
- Keep docs/provider details out of the prompt.
- Prefer compact instructions over long examples.
### Risk: tests become brittle
Mitigation:
- Test for section presence and key behavior, not every exact sentence.
- Keep exact string assertions limited to stable safety-critical phrases.
### Risk: prompt implies permissions the runtime denies
Mitigation:
- Derive access-mode wording from `SecurityPolicy::tool_availability` and current policy behavior.
- Include allowed tools in the runtime context.
- Phrase guidance as "may request when allowed" rather than unconditional permission, except in mode-specific sections verified against code.
### Risk: global instructions appear stronger than safety rules
Mitigation:
- Place global instructions in a clearly labelled section.
- State that they are followed only when consistent with user requests and runtime safety constraints.
- Add a test for this wording.
### Risk: models ignore concise instructions
Mitigation:
- Use direct imperative wording.
- Put runtime constraints near the end.
- Avoid burying editing and safety rules in long paragraphs.
## Acceptance Criteria
- `src/prompt.rs` produces a structured, readable prompt with clear sections for role, working style, transcript/tool behavior, tool use, editing, runtime context, access rules, and final responses.
- The effective prompt for each access mode is roughly 900-1,100 tokens excluding user global instructions, with an automated guardrail preventing major accidental bloat.
- User global instructions are included only when non-empty, trimmed, clearly labelled, and subordinate to runtime safety constraints.
- Runtime context includes model, access mode, launch cwd, bundled docs directory, and allowed tools.
- Access-mode guidance matches current policy for `read-only`, `workspace-edit`, and `full-access`.
- Editing instructions explicitly cover exact unique old text, minimal non-overlapping replacements, and using `write` only for new files or intentional rewrites.
- Tool-use instructions explain when to use `ls`, `grep`, `read`, `edit`, `write`, and `shell` without over-constraining the model.
- Final response guidance requires a concise user-facing response after tool work and honest reporting of verification or blockers.
- Documentation references to global instructions are accurate and do not expose the full internal prompt.
- `cargo fmt --check` and `cargo test --locked --all-targets` pass.
+298
View File
@@ -0,0 +1,298 @@
# v0.2.6 Rust Embedding API Implementation Plan
## Goal
v0.2.6 adds the first intentional public Rust API for embedding Cassady in another Rust project. A developer should be able to add Cassady as a dependency, configure a workspace/model/access mode, start a headless agent session, send user messages, receive streamed agent events, and handle approval requests without launching the interactive TUI.
Success statement:
> A small Rust program can import `cassady`, start a new headless session in a workspace, stream assistant/tool events from a turn, optionally approve shell requests, and inspect the updated conversation state using documented experimental APIs.
## Scope
### In scope
- Add an experimental embedding API module with cohesive public types instead of requiring callers to wire together internal modules directly.
- Support starting a new headless agent session from Rust code.
- Support resuming an existing conversation by id when using Cassady's existing conversation storage.
- Support running one turn at a time and streaming typed events to the host application.
- Expose approval handling for tools that require host/user consent, especially shell in `workspace-edit` mode.
- Reuse the existing config, provider, prompt, security, conversation, and tool execution paths used by the CLI/TUI.
- Provide simple builder/options types for cwd, access mode, model/base URL/API key overrides, reasoning effort, and Cassady config root.
- Add a crate-level `prelude` or clearly documented imports for common embedding use.
- Add docs and examples that show a minimal headless integration.
- Add integration tests that exercise the public API without a terminal.
### Out of scope
- Declaring the Rust API stable for semver compatibility. The API should be explicitly marked experimental in v0.2.6.
- Replacing the CLI/TUI as the primary user interface.
- Multi-agent orchestration, task queues, background daemons, schedulers, or distributed workers.
- Custom model provider traits or non-OpenAI-compatible protocols.
- User-defined custom tools or plugin loading.
- A synchronous/blocking API. The first embedding surface can require Tokio.
- Exposing low-level terminal UI internals as supported public API.
- Publishing to crates.io as part of this release unless separately requested.
## Context and Current State
Cassady already builds a library crate:
- `Cargo.toml` defines `[lib] name = "cassady" path = "src/lib.rs"`.
- `src/lib.rs` currently re-exports many internal modules directly and exposes `run()` for the CLI/TUI path.
- `src/agent.rs` contains the core async turn loop:
- `AgentSettings`
- `AgentEvent`
- `AgentCommand`
- `run_turn(...)`
- `run_turn_with_commands(...)`
- `src/app.rs` owns interactive startup, TUI state, chat creation/resume, cancellation, approval UI, and local slash commands.
- `src/conversation.rs` persists conversations as JSONL and can create/load/list chats.
- `src/config.rs` loads providers, models, active defaults, API key references, access mode, tool limits, and docs paths.
- `src/security.rs` centralizes access-mode decisions.
- `src/tools/*` implements the same tools that headless sessions should use.
The current crate can technically be imported, but the supported path is unclear: callers must know which internal modules to combine, how to create base prompts, how to load config safely, how to route approval commands, and how to consume events. v0.2.6 should add a thin, intentional API layer over these internals.
## Design Principles
1. **Thin wrapper over proven internals.** Reuse the same agent loop and policy code as the CLI so embedded behavior matches interactive behavior.
2. **Explicitly experimental.** Make the new API useful without promising final naming or long-term stability yet.
3. **Headless first.** The API should not depend on `ratatui`, terminal setup, crossterm event loops, or slash-command UI state.
4. **Host owns presentation.** Embedded callers receive typed events and decide how to display assistant chunks, tool calls, approvals, and errors.
5. **Safe defaults.** Default to `read-only`, environment-variable API keys, existing Cassady config files, and workspace-rooted paths.
6. **Approval is part of the API.** Hosts must be able to approve or deny requests rather than having Cassady assume a TUI is present.
7. **Keep the first surface small.** Prefer one clear session builder and one turn-running method over exposing every internal knob.
## Design
### Module layout
Add a new module, for example:
```rust
pub mod embedding;
pub mod prelude;
```
`src/embedding.rs` should be the supported experimental API. Existing internal modules can remain public in v0.2.6 for compatibility, but docs should steer new users toward `cassady::embedding` or `cassady::prelude`.
Suggested public surface:
```rust
pub struct SessionBuilder { ... }
pub struct Session { ... }
pub struct SessionOptions { ... }
pub struct Turn { ... }
pub enum Event { ... }
pub enum Command { ... }
pub struct ConversationInfo { ... }
```
The exact names can change during implementation, but they should avoid leaking TUI-specific terms.
### Builder and options
Provide a builder that covers common embedding setup:
```rust
let mut session = cassady::embedding::SessionBuilder::new()
.cwd("/path/to/project")
.access_mode(AccessMode::WorkspaceEdit)
.model("accounts/fireworks/models/qwen3p7-plus")
.build()
.await?;
```
Builder responsibilities:
- Resolve and canonicalize `cwd` like CLI startup.
- Load config from the default Cassady root unless an explicit root/path is supplied.
- Apply model/base URL/API key env overrides without requiring a `Cli` value from callers.
- Resolve API key availability before starting a turn and return a useful error.
- Install or locate bundled docs as needed by `Config::load` behavior.
- Create the base system prompt with `~/.cass/global.md` when starting a new conversation.
- Default access mode to config/default, then builder override, then `read-only` if no config exists.
Avoid requiring callers to import or construct `cli::Cli`.
### New and resumed sessions
Support at least:
```rust
let session = SessionBuilder::new().cwd(".").new_session().await?;
let session = SessionBuilder::new().cwd(".").resume("chat-id").await?;
```
A `Session` should expose lightweight metadata:
```rust
session.id();
session.cwd();
session.model();
session.access_mode();
session.conversation_path();
```
The conversation should continue to be persisted in the same JSONL format so CLI and library sessions can interoperate.
### Running a turn
Provide a headless one-turn API that streams events:
```rust
let mut turn = session.start_turn("Explain the crate layout").await?;
while let Some(event) = turn.next_event().await? {
match event {
Event::AssistantChunk(text) => print!("{text}"),
Event::ApprovalRequested(request) => {
turn.approve(request.id).await?;
}
Event::Finished => break,
_ => {}
}
}
let session = turn.finish().await?;
```
Alternative designs are acceptable, such as returning `(EventStream, CommandSink)` plus a completion handle, as long as examples are simple and approval commands are supported.
The wrapper can map `agent::AgentEvent` and `agent::AgentCommand` into public embedding types. It should avoid exposing internal channel mechanics unless that is the cleanest Tokio-native API.
### Event model
Expose typed events that are stable enough for hosts to build UI/logging around:
- assistant text chunks
- reasoning chunks, when provider/model returns them
- tool call started
- tool output chunk
- tool result
- approval requested
- approval resolved
- status
- turn finished
- error or turn failure
The public event type can wrap or re-export `agent::AgentEvent` initially, but the plan should prefer a dedicated type if it prevents low-level internals from becoming accidental API.
### Approval behavior
Approval requests should include:
- request id
- tool call id
- tool name
- arguments
- human-readable reason
The host should be able to approve or deny by request id. If the host drops the turn or never responds, cancellation/drop behavior should be documented.
For v0.2.6, keep approval policy aligned with `security.rs`:
- `read-only`: shell unavailable.
- `workspace-edit`: shell asks.
- `full-access`: shell allowed.
### Cancellation and drop behavior
The TUI already cancels by aborting the agent task and repairing pending records. The embedding API should define a basic behavior:
- Dropping an active turn should abort the underlying task if possible.
- A simple explicit `cancel()` method is preferred if practical.
- Conversation repair for cancelled turns can be minimal in v0.2.6, but pending tool calls must not corrupt resumed conversations.
If full parity with the TUI cancellation path is too large, document the limitation and add tests for the supported behavior.
### Error handling
Use a public result alias such as:
```rust
pub type Result<T> = std::result::Result<T, Error>;
```
The first pass may wrap `anyhow::Error`, but public errors should include enough context for embedding callers to distinguish:
- config load errors
- missing API key
- provider request errors
- conversation load/create errors
- active turn already running
- approval request not found or already resolved
Do not panic for ordinary configuration or runtime failures.
### Examples
Add at least one compilable example under `examples/`, for example `examples/headless_agent.rs`:
```rust
use cassady::prelude::*;
#[tokio::main]
async fn main() -> cassady::embedding::Result<()> {
let mut session = SessionBuilder::new()
.cwd(std::env::current_dir()?)
.access_mode(AccessMode::ReadOnly)
.build()
.await?;
let mut turn = session.start_turn("Summarize this project.").await?;
while let Some(event) = turn.next_event().await? {
if let Event::AssistantChunk(text) = event {
print!("{text}");
}
}
turn.finish().await?;
Ok(())
}
```
The example should be honest about requiring configured providers and API keys.
## Implementation Steps
1. **Define the experimental API shape.** Add `src/embedding.rs` with builder, session, turn, event, command/approval, and result/error types.
2. **Add non-CLI config loading helpers.** Refactor or add helpers in `src/config.rs` so library callers can apply overrides without constructing `cli::Cli`.
3. **Extract chat creation/resume helpers.** Move reusable prompt/global/conversation setup out of `src/app.rs` into functions usable by both TUI and embedding API.
4. **Wrap the existing agent loop.** Use `agent::run_turn_with_commands` internally and provide a host-friendly event stream plus approval methods.
5. **Handle turn lifecycle.** Ensure a session cannot run overlapping turns unless explicitly supported; persist and return the updated conversation after a turn finishes.
6. **Add cancellation/drop handling.** Provide at least a documented `cancel()` path and avoid leaving pending tool-call records in a corrupted state.
7. **Add examples and docs.** Create a headless example and a bundled docs page for the experimental Rust API.
8. **Update README and crate exports.** Add `embedding`/`prelude` exports and a short README section pointing to the new docs.
9. **Test the public surface.** Add integration tests with a mock OpenAI-compatible server and temporary config/conversation roots.
## Tests
- Unit tests for builder option precedence: default config, explicit cwd, access mode, model, base URL, API key env, and config root.
- Integration test that starts a new session and runs a turn against `wiremock`, asserting assistant chunks and persisted conversation records.
- Integration test that resumes an existing conversation through the embedding API.
- Integration test for approval flow in `workspace-edit` mode using a mock tool call that requests shell approval.
- Test that read-only sessions do not expose write/edit/shell tools through the embedded turn.
- Test that starting a second turn while one is active returns an error or is impossible by type design.
- Example compilation through `cargo test --examples` or equivalent.
## Documentation
- Add `docs/rust-api.md` or `docs/embedding.md` describing the experimental API, setup requirements, minimal example, event loop, approval handling, and limitations.
- Link the new page from `docs/README.md` and the README.
- Document that the API is experimental in v0.2.6 and may change before a stable 1.0-style library contract.
- Include a note that embedded sessions use the same `~/.cass` config and conversation storage by default.
- Mention how hosts should run `cass setup` or provide config programmatically before using the API.
## Acceptance Criteria
- A Rust binary in `examples/` can import `cassady`, create a headless session, run a turn, and stream assistant output without launching the TUI.
- Embedded sessions use the same provider, prompt, security, tool, and conversation paths as the CLI.
- Approval requests can be approved or denied programmatically.
- New public API docs and README links clearly label the surface experimental.
- CLI/TUI behavior remains unchanged.
- `cargo fmt` and `cargo test --locked --all-targets` pass.
+54 -20
View File
@@ -151,6 +151,34 @@ pub struct Config {
pub docs_dir: PathBuf,
}
#[derive(Debug, Clone, Default)]
pub struct ConfigOverrides {
pub model: Option<String>,
pub base_url: Option<String>,
pub api_key_env: Option<String>,
pub access_mode: Option<AccessMode>,
}
impl ConfigOverrides {
pub fn from_cli(cli: &Cli) -> Self {
let access_mode = if cli.readonly {
Some(AccessMode::ReadOnly)
} else if cli.workspace_edit {
Some(AccessMode::WorkspaceEdit)
} else if cli.full_access {
Some(AccessMode::FullAccess)
} else {
None
};
Self {
model: cli.model.clone(),
base_url: cli.base_url.clone(),
api_key_env: cli.api_key_env.clone(),
access_mode,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ApiKeyReference {
Env(String),
@@ -287,17 +315,29 @@ pub fn models_path(root: &Path) -> PathBuf {
impl Config {
pub fn load(cli: &Cli) -> Result<Self> {
Self::load_from_root(cass_root(), cli)
Self::load_with_overrides(cass_root(), ConfigOverrides::from_cli(cli))
}
pub fn load_from_root(root: PathBuf, cli: &Cli) -> Result<Self> {
pub fn load_with_overrides(root: PathBuf, overrides: ConfigOverrides) -> Result<Self> {
fs::create_dir_all(root.join("conversations"))
.with_context(|| format!("creating {}", root.join("conversations").display()))?;
let docs_dir = crate::docs::install(&root)?;
Self::load_from_root_with_docs(root, docs_dir, cli)
Self::load_from_root_with_docs_and_overrides(root, docs_dir, overrides)
}
pub fn load_from_root(root: PathBuf, cli: &Cli) -> Result<Self> {
Self::load_with_overrides(root, ConfigOverrides::from_cli(cli))
}
pub fn load_from_root_with_docs(root: PathBuf, docs_dir: PathBuf, cli: &Cli) -> Result<Self> {
Self::load_from_root_with_docs_and_overrides(root, docs_dir, ConfigOverrides::from_cli(cli))
}
pub fn load_from_root_with_docs_and_overrides(
root: PathBuf,
docs_dir: PathBuf,
overrides: ConfigOverrides,
) -> Result<Self> {
fs::create_dir_all(&root).with_context(|| format!("creating {}", root.display()))?;
let providers = load_or_create_default_provider_registry(&root)?;
let models = load_or_create_default_model_registry(&root)?;
@@ -330,19 +370,13 @@ impl Config {
}
}
if cli.readonly {
cfg.default_access_mode = AccessMode::ReadOnly;
}
if cli.workspace_edit {
cfg.default_access_mode = AccessMode::WorkspaceEdit;
}
if cli.full_access {
cfg.default_access_mode = AccessMode::FullAccess;
if let Some(access_mode) = overrides.access_mode {
cfg.default_access_mode = access_mode;
}
let requested_model = requested_model(file.as_ref(), cli);
let requested_model = requested_model(file.as_ref(), &overrides);
let provider_id_from_config = requested_provider_id(file.as_ref(), &providers);
let legacy = legacy_provider_override(file.as_ref(), cli);
let legacy = legacy_provider_override(file.as_ref(), &overrides);
let mut provider = resolve_provider(
requested_model.as_deref().unwrap_or(DEFAULT_MODEL),
@@ -353,10 +387,10 @@ impl Config {
&models,
)?;
if let Some(base_url) = &cli.base_url {
if let Some(base_url) = &overrides.base_url {
provider.base_url = base_url.clone();
}
if let Some(api_key_env) = &cli.api_key_env {
if let Some(api_key_env) = &overrides.api_key_env {
provider.api_key = format!("${api_key_env}");
}
@@ -706,8 +740,8 @@ pub fn find_model_for_provider<'a>(
.find(|m| m.provider == provider_id && m.id == model_id)
}
fn requested_model(file: Option<&ConfigFile>, cli: &Cli) -> Option<String> {
cli.model.clone().or_else(|| {
fn requested_model(file: Option<&ConfigFile>, overrides: &ConfigOverrides) -> Option<String> {
overrides.model.clone().or_else(|| {
file.and_then(|f| {
f.default_model
.clone()
@@ -738,13 +772,13 @@ struct LegacyProviderOverride {
fn legacy_provider_override(
file: Option<&ConfigFile>,
cli: &Cli,
overrides: &ConfigOverrides,
) -> Option<LegacyProviderOverride> {
let base_url = cli
let base_url = overrides
.base_url
.clone()
.or_else(|| file.and_then(|f| f.base_url.clone()));
let api_key = cli
let api_key = overrides
.api_key_env
.as_ref()
.map(|env| format!("${env}"))
+563
View File
@@ -0,0 +1,563 @@
//! Experimental Rust embedding API for running Cassady without the TUI.
//!
//! This module provides the first Rust-native surface for embedding Cassady in
//! another application. It reuses Cassady's existing runtime behavior while
//! giving the host application control over event presentation, turn lifecycle,
//! and approval decisions.
use crate::access::AccessMode;
use crate::agent::{self, AgentCommand, AgentEvent, AgentSettings};
use crate::config::{Config, ConfigOverrides, ReasoningEffort};
use crate::conversation::{self, Conversation, Record};
use crate::prompt;
use serde_json::Value;
use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};
use thiserror::Error;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
const TURN_CANCELLED_MESSAGE: &str = "Turn cancelled by host.";
const TOOL_CANCELLED_MESSAGE: &str = "Tool execution cancelled by host.";
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Error)]
pub enum Error {
#[error("configuration error: {0}")]
Config(#[source] anyhow::Error),
#[error("conversation error: {0}")]
Conversation(#[source] anyhow::Error),
#[error("agent error: {0}")]
Agent(#[source] anyhow::Error),
#[error("agent task failed: {0}")]
Join(#[source] tokio::task::JoinError),
#[error("turn is already closed")]
TurnClosed,
#[error("approval request `{0}` is not pending")]
ApprovalNotPending(String),
#[error("turn session state is unavailable")]
MissingSession,
}
impl Error {
fn config(err: anyhow::Error) -> Self {
Self::Config(err)
}
fn conversation(err: anyhow::Error) -> Self {
Self::Conversation(err)
}
fn agent(err: anyhow::Error) -> Self {
Self::Agent(err)
}
}
#[derive(Debug, Clone, Default)]
pub struct SessionBuilder {
config_root: Option<PathBuf>,
cwd: Option<PathBuf>,
access_mode: Option<AccessMode>,
model: Option<String>,
base_url: Option<String>,
api_key_env: Option<String>,
reasoning_effort: Option<ReasoningEffort>,
}
impl SessionBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn config_root(mut self, root: impl Into<PathBuf>) -> Self {
self.config_root = Some(root.into());
self
}
pub fn cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
self.cwd = Some(cwd.into());
self
}
pub fn access_mode(mut self, mode: AccessMode) -> Self {
self.access_mode = Some(mode);
self
}
pub fn model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = Some(base_url.into());
self
}
pub fn api_key_env(mut self, api_key_env: impl Into<String>) -> Self {
self.api_key_env = Some(api_key_env.into());
self
}
pub fn reasoning_effort(mut self, effort: ReasoningEffort) -> Self {
self.reasoning_effort = Some(effort);
self
}
pub async fn build(self) -> Result<Session> {
self.new_session().await
}
pub async fn new_session(self) -> Result<Session> {
let PreparedSession {
config,
cwd,
mode,
reasoning_effort,
} = self.prepare().await?;
let conversation = create_new_conversation(&config, &cwd)?;
Ok(Session {
config,
cwd,
mode,
reasoning_effort,
conversation,
resume_warning: None,
})
}
pub async fn resume(self, chat_id: impl AsRef<str>) -> Result<Session> {
let PreparedSession {
config,
cwd,
mode,
reasoning_effort,
} = self.prepare().await?;
let (conversation, warning) =
Conversation::load(&config.conversations_dir(), chat_id.as_ref())
.map_err(Error::conversation)?;
Ok(Session {
config,
cwd,
mode,
reasoning_effort,
conversation,
resume_warning: warning,
})
}
async fn prepare(self) -> Result<PreparedSession> {
let root = self.config_root.unwrap_or_else(crate::config::cass_root);
let overrides = ConfigOverrides {
model: self.model,
base_url: self.base_url,
api_key_env: self.api_key_env,
access_mode: self.access_mode,
};
let config = Config::load_with_overrides(root, overrides).map_err(Error::config)?;
config.resolved_api_key().map_err(Error::config)?;
let cwd = resolve_cwd(self.cwd).map_err(Error::config)?;
let mode = config.default_access_mode;
let reasoning_effort = self
.reasoning_effort
.unwrap_or(config.reasoning_effort)
.clamp_for_model(config.model_metadata.as_ref());
Ok(PreparedSession {
config,
cwd,
mode,
reasoning_effort,
})
}
}
struct PreparedSession {
config: Config,
cwd: PathBuf,
mode: AccessMode,
reasoning_effort: ReasoningEffort,
}
#[derive(Debug)]
pub struct Session {
config: Config,
cwd: PathBuf,
mode: AccessMode,
reasoning_effort: ReasoningEffort,
conversation: Conversation,
resume_warning: Option<String>,
}
impl Session {
pub fn id(&self) -> &str {
&self.conversation.id
}
pub fn cwd(&self) -> &Path {
&self.cwd
}
pub fn model(&self) -> &str {
&self.config.model
}
pub fn access_mode(&self) -> AccessMode {
self.mode
}
pub fn reasoning_effort(&self) -> ReasoningEffort {
self.reasoning_effort
}
pub fn conversation_path(&self) -> &Path {
&self.conversation.path
}
pub fn records(&self) -> &[Record] {
&self.conversation.records
}
pub fn resume_warning(&self) -> Option<&str> {
self.resume_warning.as_deref()
}
pub fn info(&self) -> ConversationInfo {
ConversationInfo {
id: self.conversation.id.clone(),
cwd: self.cwd.clone(),
model: self.config.model.clone(),
access_mode: self.mode,
reasoning_effort: self.reasoning_effort,
path: self.conversation.path.clone(),
record_count: self.conversation.records.len(),
}
}
pub async fn start_turn(self, user_message: impl Into<String>) -> Result<Turn> {
let message = user_message.into();
let turn_start_len = self.conversation.records.len();
let (event_tx, event_rx) = mpsc::unbounded_channel::<AgentEvent>();
let (command_tx, command_rx) = mpsc::unbounded_channel::<AgentCommand>();
let settings = AgentSettings {
config: self.config.clone(),
cwd: self.cwd.clone(),
mode: self.mode,
reasoning_effort: self.reasoning_effort,
};
let conversation = self.conversation.clone();
let task_message = message.clone();
let handle = tokio::spawn(agent::run_turn_with_commands(
conversation,
task_message,
settings,
event_tx,
command_rx,
));
Ok(Turn {
session: Some(self),
handle: Some(handle),
event_rx,
command_tx: Some(command_tx),
pending_approvals: BTreeSet::new(),
turn_start_len,
user_message: message,
})
}
}
#[derive(Debug, Clone)]
pub struct ConversationInfo {
pub id: String,
pub cwd: PathBuf,
pub model: String,
pub access_mode: AccessMode,
pub reasoning_effort: ReasoningEffort,
pub path: PathBuf,
pub record_count: usize,
}
#[derive(Debug)]
pub struct Turn {
session: Option<Session>,
handle: Option<JoinHandle<anyhow::Result<Conversation>>>,
event_rx: mpsc::UnboundedReceiver<AgentEvent>,
command_tx: Option<mpsc::UnboundedSender<AgentCommand>>,
pending_approvals: BTreeSet<String>,
turn_start_len: usize,
user_message: String,
}
impl Turn {
pub async fn next_event(&mut self) -> Result<Option<Event>> {
match self.event_rx.recv().await {
Some(event) => {
let event = Event::from_agent(event);
match &event {
Event::ApprovalRequested(request) => {
self.pending_approvals.insert(request.request_id.clone());
}
Event::ApprovalResolved { request_id, .. } => {
self.pending_approvals.remove(request_id);
}
_ => {}
}
Ok(Some(event))
}
None => Ok(None),
}
}
pub fn approve(&mut self, request_id: impl AsRef<str>) -> Result<()> {
self.resolve_approval(request_id.as_ref(), true)
}
pub fn deny(&mut self, request_id: impl AsRef<str>) -> Result<()> {
self.resolve_approval(request_id.as_ref(), false)
}
pub async fn finish(mut self) -> Result<Session> {
let handle = self.handle.take().ok_or(Error::TurnClosed)?;
let conversation = match handle.await.map_err(Error::Join)? {
Ok(conversation) => conversation,
Err(err) => return Err(Error::agent(err)),
};
let mut session = self.session.take().ok_or(Error::MissingSession)?;
session.conversation = conversation;
self.command_tx = None;
Ok(session)
}
pub async fn cancel(mut self) -> Result<Session> {
if let Some(handle) = &self.handle {
handle.abort();
}
if let Some(handle) = self.handle.take() {
match handle.await {
Ok(Ok(conversation)) => {
let mut session = self.session.take().ok_or(Error::MissingSession)?;
session.conversation = conversation;
self.command_tx = None;
return Ok(session);
}
Ok(Err(err)) => return Err(Error::agent(err)),
Err(err) if err.is_cancelled() => {}
Err(err) => return Err(Error::Join(err)),
}
}
let mut session = self.session.take().ok_or(Error::MissingSession)?;
session.conversation = finalize_cancelled_turn(
&session.config,
&session.conversation.id,
self.turn_start_len,
&self.user_message,
)?;
self.command_tx = None;
Ok(session)
}
fn resolve_approval(&mut self, request_id: &str, approved: bool) -> Result<()> {
if !self.pending_approvals.remove(request_id) {
return Err(Error::ApprovalNotPending(request_id.to_string()));
}
let tx = self.command_tx.as_ref().ok_or(Error::TurnClosed)?;
tx.send(AgentCommand::ApprovalDecision {
request_id: request_id.to_string(),
approved,
})
.map_err(|_| Error::TurnClosed)
}
}
impl Drop for Turn {
fn drop(&mut self) {
if let Some(handle) = &self.handle {
handle.abort();
}
}
}
#[derive(Debug, Clone)]
pub enum Event {
AssistantChunk(String),
ReasoningChunk(String),
ToolCallStarted {
id: String,
name: String,
arguments: Value,
},
ToolOutputChunk {
id: String,
name: String,
stream: String,
content: String,
},
ToolResult {
id: String,
name: String,
ok: bool,
content: String,
},
ApprovalRequested(ApprovalRequest),
ApprovalResolved {
request_id: String,
approved: bool,
},
Status(String),
Finished,
}
impl Event {
fn from_agent(event: AgentEvent) -> Self {
match event {
AgentEvent::AssistantChunk(text) => Self::AssistantChunk(text),
AgentEvent::ReasoningChunk(text) => Self::ReasoningChunk(text),
AgentEvent::ToolCallStarted {
id,
name,
arguments,
} => Self::ToolCallStarted {
id,
name,
arguments,
},
AgentEvent::ToolOutputChunk {
id,
name,
stream,
content,
} => Self::ToolOutputChunk {
id,
name,
stream,
content,
},
AgentEvent::ToolResult {
id,
name,
ok,
content,
} => Self::ToolResult {
id,
name,
ok,
content,
},
AgentEvent::ApprovalRequested {
request_id,
tool_call_id,
name,
arguments,
reason,
} => Self::ApprovalRequested(ApprovalRequest {
request_id,
tool_call_id,
name,
arguments,
reason,
}),
AgentEvent::ApprovalResolved {
request_id,
approved,
} => Self::ApprovalResolved {
request_id,
approved,
},
AgentEvent::Status(status) => Self::Status(status),
AgentEvent::TurnFinished => Self::Finished,
}
}
}
#[derive(Debug, Clone)]
pub struct ApprovalRequest {
pub request_id: String,
pub tool_call_id: String,
pub name: String,
pub arguments: Value,
pub reason: String,
}
fn resolve_cwd(cwd: Option<PathBuf>) -> anyhow::Result<PathBuf> {
let cwd = cwd.unwrap_or(std::env::current_dir()?);
cwd.canonicalize()
.map_err(anyhow::Error::from)
.map_err(|err| anyhow::anyhow!("resolving cwd {}: {err}", cwd.display()))
}
fn create_new_conversation(config: &Config, cwd: &Path) -> Result<Conversation> {
let global = fs::read_to_string(config.global_path()).ok();
let base = prompt::build_base_system_prompt(global.as_deref());
Conversation::create(&config.conversations_dir(), &config.model, cwd, base)
.map_err(Error::conversation)
}
fn finalize_cancelled_turn(
config: &Config,
chat_id: &str,
turn_start_len: usize,
turn_message: &str,
) -> Result<Conversation> {
let (mut conversation, _) =
Conversation::load(&config.conversations_dir(), chat_id).map_err(Error::conversation)?;
if conversation.records.len() <= turn_start_len {
conversation
.append(Record::User {
content: turn_message.to_string(),
ts: conversation::now_ts(),
})
.map_err(Error::conversation)?;
}
for (id, name) in pending_tool_calls(&conversation.records) {
conversation
.append(Record::Tool {
tool_call_id: id,
name,
ok: false,
content: TOOL_CANCELLED_MESSAGE.to_string(),
ts: conversation::now_ts(),
})
.map_err(Error::conversation)?;
}
if !matches!(
conversation.records.last(),
Some(Record::Assistant { content, tool_calls, .. })
if content == TURN_CANCELLED_MESSAGE && tool_calls.is_empty()
) {
conversation
.append(Record::Assistant {
content: TURN_CANCELLED_MESSAGE.to_string(),
reasoning: String::new(),
reasoning_field: None,
tool_calls: Vec::new(),
ts: conversation::now_ts(),
})
.map_err(Error::conversation)?;
}
Ok(conversation)
}
fn pending_tool_calls(records: &[Record]) -> Vec<(String, String)> {
let mut pending = Vec::new();
for record in records {
match record {
Record::Assistant { tool_calls, .. } => {
pending = tool_calls
.iter()
.map(|call| (call.id.clone(), call.name.clone()))
.collect();
}
Record::Tool { tool_call_id, .. } => {
pending.retain(|(id, _)| id != tool_call_id);
}
Record::User { .. } => pending.clear(),
_ => {}
}
}
pending
}
+2
View File
@@ -6,8 +6,10 @@ pub mod cli;
pub mod config;
pub mod conversation;
pub mod docs;
pub mod embedding;
pub mod error;
pub mod menu;
pub mod prelude;
pub mod prompt;
pub mod providers;
pub mod security;
+7
View File
@@ -0,0 +1,7 @@
//! Common imports for Cassady's experimental Rust embedding API.
pub use crate::access::AccessMode;
pub use crate::config::ReasoningEffort;
pub use crate::embedding::{
ApprovalRequest, ConversationInfo, Event, Session, SessionBuilder, Turn,
};
+48 -15
View File
@@ -3,20 +3,33 @@ use std::path::Path;
pub fn build_base_system_prompt(global: Option<&str>) -> String {
let mut prompt = String::new();
prompt.push_str("1. Identity and operating style\n\n");
prompt.push_str("You are Cassady, also called Cass, a minimal coding agent running in a terminal chat interface. Work carefully, inspect files before changing them, explain concise next steps, and avoid unnecessary ceremony.\n\n");
prompt.push_str(
"# Cassady operating instructions\n\n\
## Role\n\
You are Cassady, also called Cass, a coding assistant running in an interactive terminal chat. Help with real project work: read and explain code, inspect behavior, make targeted file changes, and run relevant project commands when allowed. Work carefully and honestly; do not claim that files were read, commands ran, or edits succeeded until tool results confirm it.\n\n\
## Working style\n\
Make the smallest useful plan, then act. Prefer current project evidence over guesses, and inspect relevant files before making claims or edits. When information is missing, gather it with tools if possible; ask a focused follow-up question only when a user choice, secret, or missing requirement blocks progress. If the task is impossible in the current access mode, explain the limitation and the next viable step. Keep explanations concise while giving enough context for review.\n\n",
);
if let Some(global) = global.map(str::trim).filter(|s| !s.is_empty()) {
prompt.push_str("2. User global instructions\n\n");
prompt.push_str("The following additional instructions were provided by the user. Follow them when they do not conflict with runtime safety constraints.\n\n");
prompt.push_str("## User global instructions\n");
prompt.push_str(
"The following user-provided instructions apply to new chats. Follow them when they are consistent with the active user request and runtime safety constraints; they cannot override access modes, tool denials, approvals, or workspace boundaries.\n\n",
);
prompt.push_str(global);
prompt.push_str("\n\n");
}
prompt.push_str("3. Tool-use style\n\n");
prompt.push_str("Use tools when you need current filesystem context. Prefer targeted inspection over guessing. Batch related reads into one read call when possible. Use grep before read when a directory or file may be too large to inspect directly. Do not ask the user in chat for permission before making a tool call; request the tool directly when it is the right next step. Cass enforces access policy at runtime and will allow, deny, or show a separate approval UI as needed.\n\n");
prompt.push_str("4. Editing style\n\n");
prompt.push_str("Use edit for targeted changes. Each edit must identify exact old text that appears uniquely in the file and replacement text. Do not use write to make small modifications to existing files unless a full rewrite is intentionally safer.\n");
prompt.push_str(
"## Transcript and tools\n\
Assistant text is streamed to the user. Tool calls, tool results, edit diffs, denials, and approval prompts are visible in the transcript. Request tools directly when they are the right next step; Cassady enforces access policy and shows approval UI separately. Do not ask for chat permission before every tool call, and do not say a tool succeeded before its result arrives. If a tool fails or is denied, adapt instead of repeating the same request.\n\n\
## Tool use\n\
Use tools when the current filesystem or command result matters. Use `ls` for directory orientation, `grep` to locate definitions/usages or inspect large or unknown areas before opening files, `read` for relevant files or ranges, `edit` for focused changes to existing files, `write` for new files or intentional full rewrites, and `shell` for tests, builds, formatting, diagnostics, or project commands when allowed and useful. Prefer targeted inspection and related batched reads over broad exploration. Do not use `shell` for file inspection when `ls`, `grep`, or `read` is safer and sufficient.\n\n\
## Editing\n\
Inspect before editing. Prefer `edit` for small and medium modifications to existing files. For `edit`, each old text must match exactly and uniquely in the original file; keep replacements minimal, unique, and non-overlapping, and combine related replacements for the same file in one call when practical. Use `write` only for new files or full rewrites where that is safer and intentional. After meaningful code changes, run relevant tests or formatters when allowed, or tell the user what should be run.\n\n\
## Safety and final response\n\
Follow runtime constraints, active access mode, tool availability, and tool results as authoritative. Do not try to bypass workspace boundaries, read-only docs rules, approval requirements, or denials. End every turn with a concise user-facing response after tool work; do not finish with only tool calls. Summarize what changed, where, and how it was verified, or summarize findings, blockers, skipped tests, and assumptions if no change was made.\n",
);
prompt
}
@@ -30,23 +43,43 @@ pub fn build_effective_system_prompt(
) -> String {
let mut prompt = String::new();
prompt.push_str(base.trim_end());
prompt.push_str("\n\n5. Current runtime constraints\n\n");
prompt.push_str("\n\n## Runtime context\n");
prompt.push_str(&format!("Model: {model}.\n"));
prompt.push_str(&format!("Access mode: {}.\n", mode.as_str()));
prompt.push_str(&format!("Launch working directory: {}.\n", cwd.display()));
prompt.push_str(&format!(
"Bundled Cass docs directory: {}. This directory is read-only for tools. Use ls, read, and grep there when you need Cass documentation.\n",
"Bundled Cass docs directory: {}. Use this directory for Cass documentation; write/edit are blocked there.\n",
docs_dir.display()
));
prompt.push_str(&format!(
"Allowed tools this turn: {}.\n\n",
allowed_tools.join(", ")
render_allowed_tools(allowed_tools)
));
prompt.push_str("## Access rules for this session\n");
match mode {
AccessMode::ReadOnly => prompt.push_str("In read-only mode, you may inspect files with ls, read, and grep only inside the launch working directory or bundled Cass docs directory. Do not request write, edit, or shell. If a task requires modification, explain that a more permissive mode is needed.\n\n"),
AccessMode::WorkspaceEdit => prompt.push_str("In workspace-edit mode, you may inspect files with ls, read, and grep only inside the launch working directory or bundled Cass docs directory. You may write and edit files only inside the launch working directory. Bundled Cass docs are read-only. You may request shell when useful. Do not ask the user for shell permission in chat; call the shell tool directly and Cass will handle any required approval separately before execution.\n\n"),
AccessMode::FullAccess => prompt.push_str("In full-access mode, you may request ls, read, grep, write, edit, and shell when needed. The shell tool runs commands in the launch working directory. Cass does not restrict read paths to the launch directory, but normal operating-system permissions still apply. write and edit are still blocked under the bundled Cass docs directory.\n\n"),
AccessMode::ReadOnly => prompt.push_str(
"Read-only mode permits inspection only. Use `ls`, `read`, and `grep` only inside the launch workspace or bundled Cass docs directory. Do not request `write`, `edit`, or `shell`. If changes, commands, or out-of-scope paths are needed, explain that a more permissive access mode is required.\n\n",
),
AccessMode::WorkspaceEdit => prompt.push_str(
"Workspace-edit mode permits `ls`, `read`, and `grep` inside the launch workspace and bundled Cass docs directory. Write/edit only inside the launch workspace; bundled docs remain read-only. Shell may be requested when useful, but Cassady handles the approval UI, so do not ask for shell permission in chat first. If a path escapes the workspace, choose an in-workspace alternative or explain the limitation.\n\n",
),
AccessMode::FullAccess => prompt.push_str(
"Full-access mode permits `ls`, `read`, `grep`, `write`, `edit`, and `shell` when needed. Shell runs from the launch working directory, and normal operating-system permissions still apply. Bundled docs remain read-only for write/edit. Even in full-access, keep changes targeted and avoid destructive commands unless the user explicitly requested them and they are necessary.\n\n",
),
}
prompt.push_str("6. Response behavior\n\nAssistant output is streamed to the user. Keep user-facing text direct and useful. Tool calls and results are visible to the user, so avoid claiming work happened until the relevant tool result confirms it. After using tools or completing requested work, always end the turn with a concise final user-facing response. Do not finish a turn with only tool calls.\n");
prompt.push_str(
"## Runtime authority\n\
Runtime policy and tool results override general guidance and user global instructions. The allowed-tools list is the source of truth for this turn; if Cassady denies a tool or path, adapt and report the limitation. Always provide a concise final response after tool activity.\n",
);
prompt
}
fn render_allowed_tools(allowed_tools: &[String]) -> String {
if allowed_tools.is_empty() {
"<none>".into()
} else {
allowed_tools.join(", ")
}
}
+1
View File
@@ -59,6 +59,7 @@ fn expected_bundled_docs_exist() {
"configuration.md",
"providers.md",
"access-modes.md",
"embedding.md",
"workflows.md",
"troubleshooting.md",
"platforms.md",
+315
View File
@@ -0,0 +1,315 @@
use cassady::access::AccessMode;
use cassady::config::ReasoningEffort;
use cassady::conversation::Record;
use cassady::embedding::{Event, SessionBuilder};
use serde_json::json;
use tempfile::tempdir;
use wiremock::matchers::{body_string_contains, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn sse(body: &str) -> ResponseTemplate {
ResponseTemplate::new(200).set_body_raw(body.as_bytes().to_vec(), "text/event-stream")
}
fn content_sse(content: &str) -> ResponseTemplate {
sse(&format!(
"data: {{\"choices\":[{{\"index\":0,\"delta\":{{\"content\":{}}}}}]}}\r\n\r\ndata: [DONE]\r\n\r\n",
serde_json::to_string(content).unwrap()
))
}
fn tool_call_sse(id: &str, name: &str, arguments: &str) -> ResponseTemplate {
sse(&format!(
"data: {{\"choices\":[{{\"index\":0,\"delta\":{{\"tool_calls\":[{{\"index\":0,\"id\":\"{id}\",\"type\":\"function\",\"function\":{{\"name\":\"{name}\",\"arguments\":{}}}}}]}}}}]}}\r\n\r\ndata: [DONE]\r\n\r\n",
serde_json::to_string(arguments).unwrap()
))
}
fn write_test_config(root: &std::path::Path, base_url: &str) {
std::fs::write(
root.join("providers.json"),
serde_json::to_string_pretty(&json!({
"providers": [{
"id": "test-provider",
"kind": "openai-compatible",
"base_url": base_url,
"api_key": "test-key",
"default_model": "test-model",
"models": ["test-model"]
}]
}))
.unwrap(),
)
.unwrap();
std::fs::write(
root.join("models.json"),
serde_json::to_string_pretty(&json!({
"models": [{
"id": "test-model",
"provider": "test-provider",
"context_length": 128,
"max_output_tokens": 64,
"reasoning": {
"supported": true,
"required": false,
"default_effort": "off",
"request_format": "reasoning_effort"
}
}]
}))
.unwrap(),
)
.unwrap();
std::fs::write(
root.join("config.json"),
serde_json::to_string_pretty(&json!({
"default_provider": "test-provider",
"default_model": "test-model",
"default_reasoning_effort": "off"
}))
.unwrap(),
)
.unwrap();
}
#[tokio::test]
async fn embedded_session_runs_turn_and_streams_events() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/chat/completions"))
.respond_with(content_sse("Hello from embedded Cassady."))
.expect(1)
.mount(&server)
.await;
let root = tempdir().unwrap();
let cwd = tempdir().unwrap();
write_test_config(root.path(), &server.uri());
let session = SessionBuilder::new()
.config_root(root.path())
.cwd(cwd.path())
.access_mode(AccessMode::ReadOnly)
.reasoning_effort(ReasoningEffort::Off)
.build()
.await
.unwrap();
assert_eq!(session.model(), "test-model");
assert_eq!(session.access_mode(), AccessMode::ReadOnly);
let mut turn = session.start_turn("say hi").await.unwrap();
let mut streamed = String::new();
while let Some(event) = turn.next_event().await.unwrap() {
match event {
Event::AssistantChunk(chunk) => streamed.push_str(&chunk),
Event::Finished => break,
_ => {}
}
}
let session = turn.finish().await.unwrap();
assert_eq!(streamed, "Hello from embedded Cassady.");
assert!(session.records().iter().any(|record| matches!(
record,
Record::Assistant { content, .. } if content == "Hello from embedded Cassady."
)));
assert!(session.conversation_path().is_file());
}
#[tokio::test]
async fn builder_overrides_config_for_model_endpoint_key_mode_and_reasoning() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/chat/completions"))
.and(body_string_contains("\"model\":\"test-model\""))
.and(body_string_contains("\"reasoning_effort\":\"low\""))
.respond_with(content_sse("Overrides worked."))
.expect(1)
.mount(&server)
.await;
let root = tempdir().unwrap();
let cwd = tempdir().unwrap();
write_test_config(root.path(), "https://wrong.example/v1");
let env_name = "CASSADY_EMBEDDING_TEST_KEY";
let old = std::env::var(env_name).ok();
std::env::set_var(env_name, "test-key-from-env");
let session = SessionBuilder::new()
.config_root(root.path())
.cwd(cwd.path())
.access_mode(AccessMode::WorkspaceEdit)
.model("test-model")
.base_url(server.uri())
.api_key_env(env_name)
.reasoning_effort(ReasoningEffort::Low)
.build()
.await
.unwrap();
assert_eq!(session.access_mode(), AccessMode::WorkspaceEdit);
assert_eq!(session.reasoning_effort(), ReasoningEffort::Low);
let mut turn = session.start_turn("check overrides").await.unwrap();
while let Some(event) = turn.next_event().await.unwrap() {
if matches!(event, Event::Finished) {
break;
}
}
let _session = turn.finish().await.unwrap();
if let Some(old) = old {
std::env::set_var(env_name, old);
} else {
std::env::remove_var(env_name);
}
}
#[tokio::test]
async fn embedded_session_can_resume_existing_conversation() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/chat/completions"))
.respond_with(content_sse("First turn."))
.expect(1)
.mount(&server)
.await;
let root = tempdir().unwrap();
let cwd = tempdir().unwrap();
write_test_config(root.path(), &server.uri());
let session = SessionBuilder::new()
.config_root(root.path())
.cwd(cwd.path())
.access_mode(AccessMode::ReadOnly)
.build()
.await
.unwrap();
let mut turn = session.start_turn("first").await.unwrap();
while let Some(event) = turn.next_event().await.unwrap() {
if matches!(event, Event::Finished) {
break;
}
}
let session = turn.finish().await.unwrap();
let id = session.id().to_string();
let record_count = session.records().len();
let resumed = SessionBuilder::new()
.config_root(root.path())
.cwd(cwd.path())
.resume(&id)
.await
.unwrap();
assert_eq!(resumed.id(), id);
assert_eq!(resumed.records().len(), record_count);
assert!(resumed.resume_warning().is_none());
}
#[tokio::test]
async fn embedded_approval_flow_can_approve_shell() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/chat/completions"))
.and(body_string_contains("exit code: 0"))
.respond_with(content_sse("Approved shell."))
.with_priority(1)
.expect(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/chat/completions"))
.respond_with(tool_call_sse(
"call_shell",
"shell",
r#"{"command":"touch marker"}"#,
))
.with_priority(10)
.expect(1)
.mount(&server)
.await;
let root = tempdir().unwrap();
let cwd = tempdir().unwrap();
write_test_config(root.path(), &server.uri());
let marker = cwd.path().join("marker");
let session = SessionBuilder::new()
.config_root(root.path())
.cwd(cwd.path())
.access_mode(AccessMode::WorkspaceEdit)
.build()
.await
.unwrap();
let mut turn = session.start_turn("run shell").await.unwrap();
let mut saw_request = false;
let mut saw_resolved = false;
while let Some(event) = turn.next_event().await.unwrap() {
match event {
Event::ApprovalRequested(request) => {
saw_request = true;
assert_eq!(request.name, "shell");
assert!(!marker.exists());
turn.approve(&request.request_id).unwrap();
}
Event::ApprovalResolved { approved, .. } => {
saw_resolved = approved;
}
Event::Finished => break,
_ => {}
}
}
let session = turn.finish().await.unwrap();
assert!(saw_request);
assert!(saw_resolved);
assert!(marker.exists());
assert!(session.records().iter().any(|record| matches!(
record,
Record::Tool { name, ok, content, .. }
if name == "shell" && *ok && content.contains("exit code: 0")
)));
}
#[tokio::test]
async fn read_only_embedding_does_not_advertise_mutating_tools() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/chat/completions"))
.respond_with(content_sse("Readonly."))
.expect(1)
.mount(&server)
.await;
let root = tempdir().unwrap();
let cwd = tempdir().unwrap();
write_test_config(root.path(), &server.uri());
let session = SessionBuilder::new()
.config_root(root.path())
.cwd(cwd.path())
.access_mode(AccessMode::ReadOnly)
.build()
.await
.unwrap();
let mut turn = session.start_turn("inspect only").await.unwrap();
while let Some(event) = turn.next_event().await.unwrap() {
if matches!(event, Event::Finished) {
break;
}
}
let _session = turn.finish().await.unwrap();
let requests = server.received_requests().await.unwrap();
let body = String::from_utf8_lossy(&requests[0].body);
assert!(body.contains("\"name\":\"ls\""));
assert!(body.contains("\"name\":\"read\""));
assert!(body.contains("\"name\":\"grep\""));
assert!(!body.contains("\"name\":\"write\""));
assert!(!body.contains("\"name\":\"edit\""));
assert!(!body.contains("\"name\":\"shell\""));
}
+155
View File
@@ -0,0 +1,155 @@
use cassady::access::AccessMode;
use cassady::prompt::{build_base_system_prompt, build_effective_system_prompt};
use std::path::Path;
fn approximate_token_count(s: &str) -> usize {
s.split_whitespace().count() * 4 / 3
}
fn effective_prompt(mode: AccessMode, allowed_tools: &[&str]) -> String {
let base = build_base_system_prompt(None);
let allowed_tools = allowed_tools
.iter()
.map(|tool| tool.to_string())
.collect::<Vec<_>>();
build_effective_system_prompt(
&base,
mode,
Path::new("/workspace/project"),
Path::new("/home/user/.cass/docs"),
"test-model",
&allowed_tools,
)
}
#[test]
fn base_prompt_has_required_sections_without_runtime_context() {
let prompt = build_base_system_prompt(None);
for heading in [
"# Cassady operating instructions",
"## Role",
"## Working style",
"## Transcript and tools",
"## Tool use",
"## Editing",
"## Safety and final response",
] {
assert!(prompt.contains(heading), "missing {heading}");
}
assert!(prompt.contains("You are Cassady, also called Cass"));
assert!(prompt.contains("inspect relevant files before making claims or edits"));
assert!(prompt.contains(
"Tool calls, tool results, edit diffs, denials, and approval prompts are visible"
));
assert!(prompt.contains("each old text must match exactly and uniquely"));
assert!(prompt.contains("End every turn with a concise user-facing response"));
assert!(!prompt.contains("## Runtime context"));
assert!(!prompt.contains("Model:"));
assert!(!prompt.contains("Access mode:"));
}
#[test]
fn global_instructions_are_trimmed_labelled_and_subordinate() {
let prompt = build_base_system_prompt(Some(" Keep replies terse.\n "));
assert!(prompt.contains("## User global instructions"));
assert!(prompt.contains("\nKeep replies terse.\n"));
assert!(!prompt.contains(" Keep replies terse.\n "));
assert!(prompt.contains(
"cannot override access modes, tool denials, approvals, or workspace boundaries"
));
}
#[test]
fn empty_global_instructions_are_omitted() {
let prompt = build_base_system_prompt(Some(" \n\t "));
assert!(!prompt.contains("## User global instructions"));
}
#[test]
fn effective_prompt_includes_runtime_context_and_allowed_tools() {
let prompt = effective_prompt(AccessMode::WorkspaceEdit, &["ls", "read", "grep", "edit"]);
assert!(prompt.contains("## Runtime context"));
assert!(prompt.contains("Model: test-model."));
assert!(prompt.contains("Access mode: workspace-edit."));
assert!(prompt.contains("Launch working directory: /workspace/project."));
assert!(prompt.contains("Bundled Cass docs directory: /home/user/.cass/docs."));
assert!(prompt.contains("Allowed tools this turn: ls, read, grep, edit."));
}
#[test]
fn each_access_mode_gets_matching_guidance() {
let read_only = effective_prompt(AccessMode::ReadOnly, &["ls", "read", "grep"]);
assert!(read_only.contains("Read-only mode permits inspection only"));
assert!(read_only.contains("Do not request `write`, `edit`, or `shell`"));
assert!(read_only.contains("a more permissive access mode is required"));
let workspace_edit = effective_prompt(
AccessMode::WorkspaceEdit,
&["ls", "read", "grep", "write", "edit", "shell"],
);
assert!(workspace_edit.contains("Write/edit only inside the launch workspace"));
assert!(workspace_edit.contains("bundled docs remain read-only"));
assert!(workspace_edit.contains("Cassady handles the approval UI"));
assert!(workspace_edit.contains("do not ask for shell permission in chat first"));
let full_access = effective_prompt(
AccessMode::FullAccess,
&["ls", "read", "grep", "write", "edit", "shell"],
);
assert!(full_access
.contains("Full-access mode permits `ls`, `read`, `grep`, `write`, `edit`, and `shell`"));
assert!(full_access.contains("Shell runs from the launch working directory"));
assert!(full_access.contains("Bundled docs remain read-only for write/edit"));
assert!(full_access
.contains("avoid destructive commands unless the user explicitly requested them"));
}
#[test]
fn runtime_constraints_stay_after_global_instructions() {
let base = build_base_system_prompt(Some("Prefer bullet summaries."));
let prompt = build_effective_system_prompt(
&base,
AccessMode::WorkspaceEdit,
Path::new("/workspace/project"),
Path::new("/home/user/.cass/docs"),
"test-model",
&["ls".into()],
);
let global_index = prompt.find("Prefer bullet summaries.").unwrap();
let runtime_index = prompt.find("## Runtime context").unwrap();
let access_index = prompt.find("## Access rules for this session").unwrap();
let authority_index = prompt.find("## Runtime authority").unwrap();
assert!(global_index < runtime_index);
assert!(runtime_index < access_index);
assert!(access_index < authority_index);
}
#[test]
fn effective_prompt_size_remains_intentional() {
for mode in [
AccessMode::ReadOnly,
AccessMode::WorkspaceEdit,
AccessMode::FullAccess,
] {
let prompt = effective_prompt(mode, &["ls", "read", "grep", "write", "edit", "shell"]);
let tokens = approximate_token_count(&prompt);
assert!(
(800..=1250).contains(&tokens),
"{mode} prompt had {tokens} approximate tokens"
);
}
}
#[test]
fn empty_allowed_tools_list_is_explicit() {
let prompt = effective_prompt(AccessMode::ReadOnly, &[]);
assert!(prompt.contains("Allowed tools this turn: <none>."));
}