Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,6 @@ LLM communication is delegated to ratatoskr; `gateway.rs` bridges chibi's types
- `(tein json)` exports `json-parse` and `json-stringify` (not `json-read-string`). `(tein safe-regexp)` exports `regexp`, `regexp-search`, `regexp-matches?`, `regexp-replace`, `regexp-replace-all`, `regexp-split`, `regexp-extract`, `regexp-fold`, `regexp-match-submatch`, `regexp-match->list`. Requires `regex` cargo feature on tein dep.
- `build_sandboxed_harness_context()` in `synthesised.rs` is the `pub(crate)` bridge for `eval.rs` — wraps `build_tein_context("", Sandboxed)` so eval can add its own prelude without duplicating FFI setup.
- `scheme_eval` and `execute_synthesised` return structured output: `"result: <value>\nstdout: <output>\nstderr: <output>"`. The `result` field contains the expression's return value (or `"error: ..."`). stdout/stderr show `"(empty)"` when nothing was captured. `format_eval()` stringifies via `to_string()`; `format_tool()` unwraps scheme strings via `as_string()`. `TeinSession::with_capture` uses flush-then-drain-run-flush to isolate each call's output (flush-output-port, R7RS, works in sandboxed contexts). Test helpers: use `extract_result_field(output)` (splits on `"\nstdout: "`) to isolate the result value.
- `harness-tools-docs` is a docs alist (same convention as `introspect-docs`) available in every synthesised-tool and `scheme_eval` context. Covers `define-tool`, `call-tool`, `register-hook`, `generate-id`, and `current-timestamp`. Use `(describe harness-tools-docs)` to list the full public harness API, or `(module-doc harness-tools-docs 'define-tool)` for a specific entry. `describe` takes an alist directly — NOT a symbol.
- `(module-exports '(harness tools))` errors — `(harness tools)` is runtime-registered and absent from tein's build-time `MODULE_EXPORTS` table. Use `harness-tools-docs` for API discovery instead.
- `(harness docs)` is the canonical import for harness API discovery: `(import (harness docs))` then `(describe hooks-docs)` to list all hook points with payload/return contracts, `(module-doc hooks-docs 'pre_message)` for a specific hook, or `(describe harness-tools-docs)` for the harness tool API (`define-tool`, `call-tool`, `register-hook`, etc.). Both `hooks-docs` and `harness-tools-docs` are also available as top-level bindings (pre-imported in `EVAL_PRELUDE`) but `(harness docs)` is the documented access path. `describe` takes an alist directly — NOT a symbol.
- `hooks-docs` is generated at startup from `HOOK_METADATA` (`hooks.rs`) — the single source of truth for all hook contracts. `docs/hooks.md` hook reference is also generated from it via `just generate-docs`. Adding a `HookPoint` variant without a `HOOK_METADATA` entry fails `test_hook_metadata_completeness`.
- `(module-exports '(harness docs))` (and `'(harness tools)`, `'(harness hooks)`) errors — runtime-registered modules are absent from tein's build-time `MODULE_EXPORTS` table. Use `harness-tools-docs` and `hooks-docs` for API discovery instead.
7 changes: 5 additions & 2 deletions crates/chibi-core/prompts/chibi.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,11 @@
- (assoc "key" args) extracts call arguments; keys are strings, not symbols
- call-tool invokes other registered tools: (call-tool "name" '(("arg" . "val")))
- tools register automatically on write — no restart needed, live on next turn
- runtime API docs available in every context: (describe harness-tools-docs)
- or: (module-doc harness-tools-docs 'define-tool) for a specific entry
- API and hook discovery via (harness docs) — pre-imported, no explicit import needed:
- (describe hooks-docs) → list all hook points with payload/return contracts
- (module-doc hooks-docs 'pre_message) → contract for a specific hook point
- (describe harness-tools-docs) → list harness API (define-tool, call-tool, register-hook, etc.)
- (module-doc harness-tools-docs 'define-tool) → doc for a specific harness API entry
- important: (describe X) takes a docs alist directly — NOT a symbol
- helper utilities: (generate-id) → 8-hex-char random id, (current-timestamp) → "YYYYMMDD-HHMMz" UTC
- single-tool example:
Expand Down
116 changes: 116 additions & 0 deletions crates/chibi-core/src/tools/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,122 @@ mod tests {
}
}

// --- (harness docs) module tests ---

#[test]
fn test_harness_docs_available_sandboxed() {
// (import (harness docs)) must succeed in sandboxed context
let (session, _) =
crate::tools::synthesised::build_sandboxed_harness_context().expect("build context");
let result = session
.evaluate("(import (harness docs)) #t")
.expect("import (harness docs)");
assert_eq!(result, tein::Value::Boolean(true));
}

#[test]
fn test_harness_docs_hooks_docs_bound_sandboxed() {
// hooks-docs must be a non-empty pair in sandboxed context
let (session, _) =
crate::tools::synthesised::build_sandboxed_harness_context().expect("build context");
let result = session
.evaluate("(pair? hooks-docs)")
.expect("evaluate pair?");
assert_eq!(result, tein::Value::Boolean(true));
}

#[test]
fn test_harness_docs_harness_tools_docs_bound() {
// harness-tools-docs must still be accessible after (harness docs) import
let (session, _) =
crate::tools::synthesised::build_sandboxed_harness_context().expect("build context");
let result = session
.evaluate("(pair? harness-tools-docs)")
.expect("evaluate pair?");
assert_eq!(result, tein::Value::Boolean(true));
}

#[test]
fn test_hooks_docs_module_doc() {
// (module-doc hooks-docs 'pre_message) must return a doc string
let (session, _) =
crate::tools::synthesised::build_sandboxed_harness_context().expect("build context");
let result = session
.evaluate("(module-doc hooks-docs 'pre_message)")
.expect("evaluate module-doc");
match result {
tein::Value::String(s) => {
assert!(
!s.is_empty(),
"module-doc hooks-docs pre_message must be non-empty"
);
assert!(
s.contains("message") || s.contains("prompt"),
"expected pre_message doc to mention message or prompt: {s}"
);
}
other => panic!("expected string from module-doc, got: {other:?}"),
}
}

#[test]
fn test_hooks_docs_describe_mentions_hooks() {
// (describe hooks-docs) must mention known hook names
let (session, _) =
crate::tools::synthesised::build_sandboxed_harness_context().expect("build context");
let result = session
.evaluate("(describe hooks-docs)")
.expect("evaluate describe");
match result {
tein::Value::String(s) => {
assert!(
s.contains("pre_message") && s.contains("post_tool"),
"describe hooks-docs must mention pre_message and post_tool: {s}"
);
}
other => panic!("expected string from describe, got: {other:?}"),
}
}

#[test]
fn test_hooks_docs_markdown_freshness() {
// The generated hook reference section must match what's in docs/hooks.md.
// Run `just generate-docs` to regenerate after changing HOOK_METADATA.
use crate::tools::generate_hooks_markdown;

let generated = generate_hooks_markdown();

// Find docs/hooks.md relative to the manifest directory
let hooks_md_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("docs/hooks.md");

let hooks_md = std::fs::read_to_string(&hooks_md_path)
.unwrap_or_else(|_| panic!("could not read {}", hooks_md_path.display()));

const BEGIN_MARKER: &str =
"<!-- BEGIN GENERATED HOOK REFERENCE — do not edit, run `just generate-docs` -->";
const END_MARKER: &str = "<!-- END GENERATED HOOK REFERENCE -->";

let begin_pos = hooks_md.find(BEGIN_MARKER).unwrap_or_else(|| {
panic!("BEGIN GENERATED marker not found in docs/hooks.md — run `just generate-docs`")
});
let end_pos = hooks_md.find(END_MARKER).unwrap_or_else(|| {
panic!("END GENERATED marker not found in docs/hooks.md — run `just generate-docs`")
});

let existing = &hooks_md[begin_pos + BEGIN_MARKER.len()..end_pos];
let expected = format!("\n{generated}\n");

assert_eq!(
existing, expected,
"docs/hooks.md generated section is stale — run `just generate-docs` to update"
);
}

#[test]
fn test_evict_eval_context() {
// Insert a context, evict it, verify it's gone and a fresh one is created.
Expand Down
Loading
Loading