mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
Fix Telegram Markdown formatting and clarify tool/memory distinctions
- Add escape_telegram_markdown() to handle underscores in tool names (e.g., build_software was breaking Telegram's Markdown parser) - Use Telegram-compatible *bold* syntax instead of **bold** - Clarify workspace memory vs filesystem tool descriptions to prevent LLM from using read_file on memory_tree paths - Update build_software to strongly prefer Rust WASM for agent tools - Rewrite WASM tool template to use Component Model with wit_bindgen instead of outdated extern "C" approach Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
9edc666ee3
commit
aec42aceda
+35
-7
@@ -2,6 +2,29 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Escape special characters for Telegram's legacy Markdown.
|
||||
///
|
||||
/// In Telegram's Markdown mode, these characters have special meaning:
|
||||
/// - `_` starts/ends italic
|
||||
/// - `*` starts/ends bold
|
||||
/// - `` ` `` starts/ends code
|
||||
/// - `[` starts a link
|
||||
///
|
||||
/// We escape them with backslash so dynamic content doesn't break formatting.
|
||||
fn escape_telegram_markdown(s: &str) -> String {
|
||||
let mut result = String::with_capacity(s.len());
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'_' | '*' | '`' | '[' => {
|
||||
result.push('\\');
|
||||
result.push(c);
|
||||
}
|
||||
_ => result.push(c),
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
use futures::StreamExt;
|
||||
use tokio::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
@@ -341,17 +364,22 @@ impl Agent {
|
||||
} else {
|
||||
params_preview
|
||||
};
|
||||
// Escape Markdown special chars in dynamic values to avoid breaking
|
||||
// Telegram's Markdown parser (underscores, asterisks, backticks, brackets)
|
||||
let tool_name_escaped = escape_telegram_markdown(&tool_name);
|
||||
let description_escaped = escape_telegram_markdown(&description);
|
||||
// Params go inside a code block, so no escaping needed there
|
||||
Ok(Some(format!(
|
||||
"🔒 Tool requires approval:\n\n\
|
||||
**Tool:** {}\n\
|
||||
**Description:** {}\n\
|
||||
**Parameters:** ```\n{}\n```\n\n\
|
||||
*Tool:* {}\n\
|
||||
*Description:* {}\n\
|
||||
*Parameters:*\n```\n{}\n```\n\n\
|
||||
Reply with:\n\
|
||||
- `yes` or `approve` to allow this tool\n\
|
||||
- `always` to always allow this tool in this session\n\
|
||||
- `no` or `deny` to reject\n\n\
|
||||
• yes or approve to allow this tool\n\
|
||||
• always to always allow this tool in this session\n\
|
||||
• no or deny to reject\n\n\
|
||||
Request ID: {}",
|
||||
tool_name, description, params_truncated, request_id
|
||||
tool_name_escaped, description_escaped, params_truncated, request_id
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ impl HeartbeatRunner {
|
||||
};
|
||||
|
||||
let response = OutgoingResponse {
|
||||
content: format!("🔔 **Heartbeat Alert**\n\n{}", message),
|
||||
content: format!("🔔 *Heartbeat Alert*\n\n{}", message),
|
||||
thread_id: None,
|
||||
metadata: serde_json::json!({
|
||||
"source": "heartbeat",
|
||||
|
||||
+141
-68
@@ -343,105 +343,167 @@ Language: {language:?}
|
||||
|
||||
## WASM Tool Requirements
|
||||
|
||||
You are building a WASM tool for an autonomous agent. The tool must:
|
||||
You are building a WASM Component tool for an autonomous agent using the WASM Component Model.
|
||||
The tool MUST use `wit_bindgen` and `cargo-component` to build.
|
||||
|
||||
1. **Implement the guest interface** - Export a `run` function that takes JSON input and returns JSON output
|
||||
## Available Host Functions (from WIT interface)
|
||||
|
||||
2. **Use only available host functions**:
|
||||
- `host_log(level, message)` - Log messages (levels: debug, info, warn, error)
|
||||
- `host_time()` - Get current Unix timestamp
|
||||
- `host_http_request(method, url, headers, body)` - Make HTTP requests (if capability granted)
|
||||
- `host_workspace_read(path)` - Read from workspace (if capability granted)
|
||||
- `host_workspace_write(path, content)` - Write to workspace (if capability granted)
|
||||
- `host_get_secret(name)` - Get injected secret (if capability granted)
|
||||
|
||||
3. **Handle errors gracefully** - Return error results, never panic
|
||||
|
||||
4. **Be deterministic** - Same input should produce same output (except for time/HTTP)
|
||||
|
||||
## WASM Tool Template (Rust)
|
||||
The host provides these functions via `near::agent::host`:
|
||||
|
||||
```rust
|
||||
// Cargo.toml
|
||||
// Logging (always available)
|
||||
host::log(level: LogLevel, message: &str); // LogLevel: Trace, Debug, Info, Warn, Error
|
||||
|
||||
// Time (always available)
|
||||
host::now_millis() -> u64; // Unix timestamp in milliseconds
|
||||
|
||||
// Workspace (if capability granted)
|
||||
host::workspace_read(path: &str) -> Option<String>;
|
||||
|
||||
// HTTP (if capability granted)
|
||||
host::http_request(method: &str, url: &str, headers_json: &str, body: Option<Vec<u8>>)
|
||||
-> Result<HttpResponse, String>;
|
||||
// HttpResponse has: status: u16, headers_json: String, body: Vec<u8>
|
||||
|
||||
// Tool invocation (if capability granted)
|
||||
host::tool_invoke(alias: &str, params_json: &str) -> Result<String, String>;
|
||||
|
||||
// Secrets (if capability granted) - can only CHECK existence, not read values
|
||||
host::secret_exists(name: &str) -> bool;
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
my_tool/
|
||||
├── Cargo.toml
|
||||
├── wit/
|
||||
│ └── tool.wit # Copy from agent's wit/tool.wit
|
||||
└── src/
|
||||
└── lib.rs
|
||||
```
|
||||
|
||||
## Cargo.toml Template
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "tool_name"
|
||||
name = "my_tool"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
wit-bindgen = "0.41"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
```
|
||||
|
||||
## src/lib.rs Template
|
||||
|
||||
```rust
|
||||
// Generate bindings from the WIT interface
|
||||
wit_bindgen::generate!({
|
||||
world: "sandboxed-tool",
|
||||
path: "wit/tool.wit",
|
||||
});
|
||||
|
||||
// src/lib.rs
|
||||
use serde::{Deserialize, Serialize};
|
||||
use exports::near::agent::tool::{Guest, Request, Response};
|
||||
use near::agent::host::{self, LogLevel};
|
||||
|
||||
// Your input/output types
|
||||
#[derive(Deserialize)]
|
||||
struct Input {
|
||||
// Define your input parameters
|
||||
struct MyInput {
|
||||
// Define parameters here
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Output {
|
||||
// Define your output structure
|
||||
struct MyOutput {
|
||||
// Define output here
|
||||
}
|
||||
|
||||
// Host function imports
|
||||
extern "C" {
|
||||
fn host_log(level: i32, ptr: *const u8, len: usize);
|
||||
struct MyTool;
|
||||
|
||||
impl Guest for MyTool {
|
||||
fn execute(req: Request) -> Response {
|
||||
// Parse input
|
||||
let input: MyInput = match serde_json::from_str(&req.params) {
|
||||
Ok(i) => i,
|
||||
Err(e) => return Response {
|
||||
output: None,
|
||||
error: Some(format!("Invalid input: {}", e)),
|
||||
},
|
||||
};
|
||||
|
||||
host::log(LogLevel::Info, &format!("Processing request..."));
|
||||
|
||||
// Your implementation here
|
||||
let output = MyOutput { /* ... */ };
|
||||
|
||||
// Return success
|
||||
Response {
|
||||
output: Some(serde_json::to_string(&output).unwrap()),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn schema() -> String {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
// Define your JSON Schema here
|
||||
},
|
||||
"required": []
|
||||
}).to_string()
|
||||
}
|
||||
|
||||
fn description() -> String {
|
||||
"Description of what this tool does".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn log_info(msg: &str) {
|
||||
unsafe { host_log(1, msg.as_ptr(), msg.len()); }
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn run(input_ptr: *const u8, input_len: usize) -> *mut u8 {
|
||||
// Parse input
|
||||
let input_bytes = unsafe { std::slice::from_raw_parts(input_ptr, input_len) };
|
||||
let input: Input = match serde_json::from_slice(input_bytes) {
|
||||
Ok(i) => i,
|
||||
Err(e) => return error_response(&format!("Invalid input: {}", e)),
|
||||
};
|
||||
|
||||
// Your implementation here
|
||||
let output = Output { /* ... */ };
|
||||
|
||||
// Return output
|
||||
let json = serde_json::to_vec(&output).unwrap();
|
||||
let ptr = json.as_ptr() as *mut u8;
|
||||
std::mem::forget(json);
|
||||
ptr
|
||||
}
|
||||
|
||||
fn error_response(msg: &str) -> *mut u8 {
|
||||
let json = serde_json::json!({"error": msg}).to_string();
|
||||
let ptr = json.as_ptr() as *mut u8;
|
||||
std::mem::forget(json);
|
||||
ptr
|
||||
}
|
||||
export!(MyTool);
|
||||
```
|
||||
|
||||
## Build Commands for WASM
|
||||
## Build Commands
|
||||
|
||||
```bash
|
||||
# Add WASM target
|
||||
rustup target add wasm32-wasip2
|
||||
# Install cargo-component (one time)
|
||||
cargo install cargo-component
|
||||
|
||||
# Build
|
||||
cargo build --target wasm32-wasip2 --release
|
||||
# Build the WASM component
|
||||
cargo component build --release
|
||||
|
||||
# Output will be at: target/wasm32-wasip2/release/tool_name.wasm
|
||||
# Output: target/wasm32-wasip2/release/my_tool.wasm
|
||||
```
|
||||
|
||||
## Tool Capabilities
|
||||
## Capabilities File (my_tool.capabilities.json)
|
||||
|
||||
When defining capabilities for your tool, specify which host functions it needs:
|
||||
- `http`: Allows HTTP requests to specified endpoints
|
||||
- `workspace`: Allows reading/writing workspace files
|
||||
- `secrets`: Allows accessing injected secrets
|
||||
Create alongside the .wasm file to grant capabilities:
|
||||
|
||||
```json
|
||||
{
|
||||
"http": {
|
||||
"allowed_endpoints": [
|
||||
{"host": "api.example.com", "path_prefix": "/v1/"}
|
||||
]
|
||||
},
|
||||
"workspace": true,
|
||||
"secrets": {
|
||||
"allowed": ["API_KEY"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. NEVER panic - always return Response with error field set
|
||||
2. Secrets are NEVER exposed to WASM - use placeholders like `{API_KEY}` in URLs
|
||||
and the host will inject the real value
|
||||
3. HTTP requests are rate-limited and only allowed to endpoints in capabilities
|
||||
4. Keep the tool focused on one thing - small, composable tools are better
|
||||
|
||||
"#
|
||||
.to_string()
|
||||
@@ -714,11 +776,20 @@ impl SoftwareBuilder for LlmSoftwareBuilder {
|
||||
|
||||
Description: {}
|
||||
|
||||
IMPORTANT: If this is a "tool" that the agent will use (e.g., "calendar tool", "email tool",
|
||||
"API client tool"), you MUST use:
|
||||
- software_type: "wasm_tool"
|
||||
- language: "rust"
|
||||
|
||||
Only use cli_binary/script/library for software meant for human end-users, not agent tools.
|
||||
|
||||
Respond with a JSON object containing:
|
||||
- name: A short identifier (snake_case)
|
||||
- description: What the software should do
|
||||
- software_type: One of "wasm_tool", "cli_binary", "library", "script", "web_service"
|
||||
(PREFER "wasm_tool" for agent-usable tools)
|
||||
- language: One of "rust", "python", "typescript", "javascript", "go", "bash"
|
||||
(PREFER "rust" for wasm_tool)
|
||||
- input_spec: Expected input format (optional)
|
||||
- output_spec: Expected output format (optional)
|
||||
- dependencies: List of external dependencies needed
|
||||
@@ -806,8 +877,10 @@ impl Tool for BuildSoftwareTool {
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Build software from a description. Can create WASM tools, CLI applications, scripts, \
|
||||
and more. The builder will scaffold, implement, compile, and test the software iteratively."
|
||||
"Build software from a description. IMPORTANT: For tools the agent will use, \
|
||||
ALWAYS build Rust WASM tools (type: wasm_tool, language: rust). Only use cli_binary, \
|
||||
script, or other types for software meant for human users. The builder scaffolds, \
|
||||
implements, compiles, and tests iteratively."
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
|
||||
@@ -119,7 +119,8 @@ impl Tool for ReadFileTool {
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Read the contents of a file. Returns the file content as text. \
|
||||
"Read a file from the LOCAL FILESYSTEM. NOT for workspace memory paths \
|
||||
(use memory_read for those). Returns file content as text. \
|
||||
For large files, you can specify offset and limit to read a portion."
|
||||
}
|
||||
|
||||
@@ -239,8 +240,9 @@ impl Tool for WriteFileTool {
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Write content to a file. Creates the file if it doesn't exist, overwrites if it does. \
|
||||
Parent directories are created automatically. Use apply_patch for targeted edits to existing files."
|
||||
"Write content to a file on the LOCAL FILESYSTEM. NOT for workspace memory \
|
||||
(use memory_write for that). Creates the file if it doesn't exist, overwrites if it does. \
|
||||
Parent directories are created automatically. Use apply_patch for targeted edits."
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
@@ -342,8 +344,8 @@ impl Tool for ListDirTool {
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"List contents of a directory. Shows files and subdirectories with their sizes. \
|
||||
Use for exploring project structure."
|
||||
"List contents of a directory on the LOCAL FILESYSTEM. NOT for workspace memory \
|
||||
(use memory_tree for that). Shows files and subdirectories with their sizes."
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
|
||||
@@ -266,8 +266,10 @@ impl Tool for MemoryReadTool {
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Read a file from the workspace. Use this to read identity files, \
|
||||
heartbeat checklist, memory, daily logs, or any custom file."
|
||||
"Read a file from the workspace memory (database-backed storage). \
|
||||
Use this to read files shown by memory_tree. NOT for local filesystem files \
|
||||
(use read_file for those). Works with identity files, heartbeat checklist, \
|
||||
memory, daily logs, or any custom workspace path."
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
@@ -381,8 +383,9 @@ impl Tool for MemoryTreeTool {
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"View the workspace structure as a tree. Use this to explore \
|
||||
the workspace hierarchy and discover available files and directories."
|
||||
"View the workspace memory structure as a tree (database-backed storage). \
|
||||
Use memory_read to read files shown here, NOT read_file. \
|
||||
The workspace is separate from the local filesystem."
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
|
||||
Reference in New Issue
Block a user