Publish harness and TUI open-source

initial sync from the monorepo
This commit is contained in:
grokkybara[bot] 2026-07-16 06:46:02 +01:00
commit c68e39f604
2734 changed files with 1437016 additions and 0 deletions

View file

@ -0,0 +1,25 @@
[package]
license = "Apache-2.0"
name = "xai-grok-tools-api"
version = "0.1.220-alpha.4"
edition.workspace = true
description = "Protobuf API definitions for Grok tools"
[dependencies]
tonic-prost = { workspace = true }
# Protobuf runtime
prost = { workspace = true }
tonic = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
xai-tool-protocol = { workspace = true }
[build-dependencies]
xai-proto-build = { workspace = true }
[package.metadata.cargo-shear]
ignored = ["serde", "tonic"]
[lints]
workspace = true

View file

@ -0,0 +1,34 @@
fn main() {
xai_proto_build::configure()
.type_attribute(
".", // match every message & enum
"#[derive(serde::Serialize, serde::Deserialize)]",
)
// ToolConfigEntry is embedded in external JSON contracts (Computer Hub
// `session.bind` metadata and agent-config JSON) where sparse payloads
// must deserialize. Defaults are applied per optional field (not
// type-level) so the required `id` field still fails deserialization
// when missing instead of silently becoming "". See tests/wire_shape.rs.
.field_attribute(
".xai.grok.tools.v1.ToolConfigEntry.params_json",
"#[serde(default)]",
)
.field_attribute(
".xai.grok.tools.v1.ToolConfigEntry.name_override",
"#[serde(default)]",
)
.field_attribute(
".xai.grok.tools.v1.ToolConfigEntry.params_name_overrides",
"#[serde(default)]",
)
.field_attribute(
".xai.grok.tools.v1.ToolConfigEntry.behavior_version",
"#[serde(default)]",
)
.field_attribute(
".xai.grok.tools.v1.ToolConfigEntry.description_override",
"#[serde(default)]",
)
.compile_protos(&["proto/grok-tools.proto"], &["proto/"])
.unwrap();
}

View file

@ -0,0 +1,953 @@
syntax = "proto3";
package xai.grok.tools.v1;
// ============================================================================
// MAIN SERVICE
// ============================================================================
/// GrokToolsService provides a gRPC interface to the Grok tools runtime.
/// It supports tool execution, discovery, configuration, and lifecycle management.
service GrokToolsService {
// ========== TOOL EXECUTION ==========
/// Execute a single tool call
rpc ExecuteTool(ExecuteToolRequest) returns (ExecuteToolResponse);
/// Execute a tool with streaming output.
///
/// Currently operates as "chunked unary" the tool executes to completion,
/// then the result is streamed in byte-level chunks. True incremental
/// streaming (partial output during execution) is planned.
rpc ExecuteToolStream(ExecuteToolRequest) returns (stream ToolStreamChunk);
// ========== TOOL DISCOVERY ==========
/// List all available tools with their schemas and capabilities
rpc ListTools(ListToolsRequest) returns (ListToolsResponse);
/// Get detailed information about a specific tool
rpc GetToolInfo(GetToolInfoRequest) returns (ToolInfo);
/// Used to finalize the tool configuration, after this call
/// tools are safe to call
rpc FinalizeToolConfigRequest(FinalizeToolServerConfigRequest)
returns (FinalizeToolServerConfigResponse);
// ========== TOOL STATE ==========
/// Get the current tool state (serialized Resources).
///
/// Flushes any in-flight persistence writes and waits for any active
/// tool execution to complete before serializing, so the returned
/// snapshot is always consistent.
///
/// The returned JSON has the shape:
/// ```json
/// {
/// "params": { "<tool_id>": { ... }, ... },
/// "state": { "<tool_id>": { ... }, ... }
/// }
/// ```
rpc GetToolState(GetToolStateRequest) returns (GetToolStateResponse);
// ========== DYNAMIC TOOL MANAGEMENT ==========
/// Enable a tool (make it available for execution)
rpc EnableTool(EnableToolRequest) returns (EnableToolResponse) {
option deprecated = true;
}
/// Disable a tool (prevent execution, keep registered)
rpc DisableTool(DisableToolRequest) returns (DisableToolResponse) {
option deprecated = true;
}
// ========== TOOL OPTIONS ==========
/// Set tool-specific options (merged with defaults)
rpc SetToolOptions(SetToolOptionsRequest) returns (SetToolOptionsResponse) {
option deprecated = true;
}
/// Get current tool options (effective options after merging)
rpc GetToolOptions(GetToolOptionsRequest) returns (GetToolOptionsResponse) {
option deprecated = true;
}
/// Reset tool options to defaults
rpc ResetToolOptions(ResetToolOptionsRequest)
returns (ResetToolOptionsResponse) {
option deprecated = true;
}
// ========== TOOL NAME OVERRIDES ==========
/// Set name overrides for a single tool (display name and/or parameter names).
/// Call once per tool that needs overrides. Tools without overrides use
/// canonical names. Overrides affect runtime error messages and schema
/// property names returned to the model.
rpc SetToolOverride(SetToolOverrideRequest)
returns (SetToolOverrideResponse) {
option deprecated = true;
}
/// Clear all name overrides for a tool, reverting to canonical names.
rpc ClearToolOverride(ClearToolOverrideRequest)
returns (ClearToolOverrideResponse) {
option deprecated = true;
}
// ========== SYSTEM REMINDERS ==========
/// Enable or disable system reminders globally.
/// When disabled, both per-tool reminders and TodoNudge are suppressed.
rpc SetSystemReminders(SetSystemRemindersRequest)
returns (SetSystemRemindersResponse) {
option deprecated = true;
}
/// Get the current state of the system reminders toggle.
rpc GetSystemReminders(GetSystemRemindersRequest)
returns (GetSystemRemindersResponse) {
option deprecated = true;
}
// ========== TRUNCATION CONFIG ==========
/// Set output truncation configuration for the session.
/// Controls how much tool output is kept before truncation.
/// Call once at session setup; applies to all subsequent tool executions.
rpc SetTruncationConfig(SetTruncationConfigRequest)
returns (SetTruncationConfigResponse) {
option deprecated = true;
}
/// Get the current truncation configuration.
rpc GetTruncationConfig(GetTruncationConfigRequest)
returns (GetTruncationConfigResponse) {
option deprecated = true;
}
// ========== AGENT ==========
/// Get the agent's rendered system prompt.
/// Returns empty string if the server was constructed without an agent.
rpc GetSystemPrompt(GetSystemPromptRequest)
returns (GetSystemPromptResponse) {
option deprecated = true;
}
/// Get the agent definition metadata.
/// Returns empty/default if the server was constructed without an agent.
rpc GetAgentInfo(GetAgentInfoRequest) returns (GetAgentInfoResponse) {
option deprecated = true;
}
/// Check whether the completion requirement has been satisfied
/// for the current turn. Only meaningful if the agent has a
/// completionRequirement set.
rpc GetCompletionState(GetCompletionStateRequest)
returns (GetCompletionStateResponse) {
option deprecated = true;
}
/// Reset per-turn completion tracking. Call at the start of each
/// new model turn so the harness gets a fresh "completed" signal.
rpc ResetCompletionState(ResetCompletionStateRequest)
returns (ResetCompletionStateResponse) {
option deprecated = true;
}
/// Finalize the agent after all tool name/param overrides have been
/// applied via SetToolOverride. Renders the system prompt with the
/// current override state. After this call, SetToolOverride is
/// rejected with an error.
///
/// Not needed if the server was constructed with from_definition_file()
/// finalization happens automatically at build time.
rpc FinalizeAgent(FinalizeAgentRequest) returns (FinalizeAgentResponse) {
option deprecated = true;
}
}
// ============================================================================
// TOOL SERVER CONFIG MESSAGES (finalize-time configuration)
// ============================================================================
/// Request to finalize the tool server configuration.
/// After this call, the toolset is locked and tools are safe to call.
/// This replaces the old enable/disable/SetToolOptions pattern
/// all tool selection and configuration happens in a single call.
message FinalizeToolServerConfigRequest {
/// The tools to enable and their configuration.
/// Only tools listed here will be available after finalization.
repeated ToolConfigEntry tools = 1;
/// Optional truncation configuration for the session.
optional TruncationConfig truncation = 2;
/// Whether system reminders should be enabled (default: false).
bool system_reminders_enabled = 3;
/// Optional: pre-load tool state from a previous session.
///
/// JSON shape must match GetToolStateResponse.state_json:
/// { "params": { ... }, "state": { ... } }
///
/// Applied AFTER the toolset loads any existing state from disk,
/// so values provided here override on-disk state. Missing keys
/// are left at their defaults (merge semantics, not replace).
///
/// Typical use: pass the output of a previous GetToolState call
/// to warm-start a new session with the same FileReadTracker,
/// TodoState, etc.
optional string initial_tool_state_json = 4;
/// Behavior preset name (e.g. "current", "legacy-0.4.10").
/// Applied to all version-managed tools. Defaults to "current" when empty.
optional string behavior_preset = 5;
}
/// Per-tool configuration entry.
/// Maps to `ToolConfig` in the tools library.
message ToolConfigEntry {
/// Fully qualified tool ID: colon-separated `Namespace:tool`
/// (e.g., "GrokBuild:bash", "GrokBuild:read_file").
///
/// The whole selection path keys on this format: the default
/// client-facing name is the segment after the FIRST colon (see
/// `default_client_name` in the xai-grok-tools-api crate).
string id = 1;
/// Tool-specific parameters as JSON string.
/// Merged with the tool's default params. Optional.
optional string params_json = 2;
/// Client-facing tool name override.
/// If empty, the tool's default ID is used.
optional string name_override = 3;
/// Parameter name overrides (canonical param client-facing param).
/// Only include params that need overriding.
map<string, string> params_name_overrides = 4;
/// Per-tool behavior version override (e.g. "legacy-0.4.10").
/// Wins over FinalizeToolServerConfigRequest.behavior_preset.
/// Only valid for version-managed tools.
optional string behavior_version = 5;
/// Client-facing tool description override.
/// When set, replaces the tool's built-in description entirely.
/// The override string is still rendered through TemplateRenderer,
/// so ${{ tools.by_kind.* }} variables resolve correctly.
optional string description_override = 6;
}
/// Structured validation details returned when finalize rejects a tool config.
message FinalizeConfigValidationDetails {
repeated FinalizeConfigViolation violations = 1;
}
/// One validation violation in FinalizeToolServerConfigRequest.
message FinalizeConfigViolation {
/// Zero-based index into FinalizeToolServerConfigRequest.tools.
optional uint32 entry_index = 1;
/// Fully-qualified tool ID or synthetic scope like "(global)".
string tool_id = 2;
/// Request/config field path such as "tools[0].params_json" or "params.enabled_background".
optional string field_path = 3;
/// Human-readable reason for the violation.
string message = 4;
/// Expected type/constraint when known.
optional string expected = 5;
/// Preview of the rejected value as JSON/text when safe to include.
optional string bad_value_json = 6;
/// Machine-friendly category (e.g. "params_type", "params_json_parse").
string category = 7;
}
/// Structured deprecation/lifecycle warning for a specific tool version
/// or bundle. Informational only does not affect control flow.
message VersionWarning {
/// Fully-qualified tool ID (e.g. "GrokBuild:run_terminal_cmd").
/// Empty for bundle-level warnings.
string fq_tool_id = 1;
/// The deprecated version (e.g. "legacy-0.4.10").
string deprecated_version = 2;
/// Suggested replacement version (e.g. "current").
string replacement = 3;
/// Human-readable deprecation message.
string message = 4;
}
/// Response from finalizing the tool server configuration.
message FinalizeToolServerConfigResponse {
bool success = 1;
string message = 2;
/// The finalized tool definitions (name, description, schema).
/// Clients can use these to build tool schemas for the model.
repeated ToolInfo tools = 3;
/// Deprecation/lifecycle warnings for resolved versions.
/// Empty when all versions are Active.
repeated VersionWarning version_warnings = 4;
}
// ============================================================================
// TOOL EXECUTION MESSAGES
// ============================================================================
message ExecuteToolRequest {
/// Name of the tool to execute (e.g., "read_file", "grep", "run_terminal_cmd")
string tool_name = 1;
/// Tool input parameters as JSON string
string input_json = 2;
/// Reserved for future session support
reserved 3;
/// Execution options (global)
ExecutionOptions options = 4;
/// Unique call ID for tracing (optional, generated if not provided)
optional string call_id = 5;
// Field 6 reserved: was tool_options_json, removed.
// Use ToolConfigEntry.params_json in FinalizeToolServerConfigRequest instead.
reserved 6;
}
message ExecutionOptions {
/// Timeout in milliseconds
optional uint64 timeout_ms = 1;
/// Whether to run in background (for bash)
bool background = 2;
/// Base output format preference
OutputFormat output_format = 3;
/// Working directory override (for file operations)
optional string working_directory = 4;
/// Specific fields to include in output_json (e.g., ["path", "lines", "line_count"])
/// If empty, includes all fields based on output_format.
/// Field names are tool-specific; check tool's output schema for available fields.
repeated string include_fields = 5;
/// Fields to exclude from output_json (e.g., ["raw_bytes", "metadata"])
/// Applied after include_fields filtering.
/// Useful for removing large/unnecessary fields to save tokens.
repeated string exclude_fields = 6;
/// Maximum size of each streaming data chunk in bytes.
/// Default: 1 MiB (1048576). Must not exceed 4 MiB (4194304).
/// Only used by ExecuteToolStream; ignored by ExecuteTool.
optional uint32 stream_chunk_size = 7;
}
/// Client-configurable truncation settings.
/// All fields are optional omitted fields mean "use the tool's built-in default".
message TruncationConfig {
/// Max total output bytes for any tool. Default: 40KB.
/// Overrides the built-in per-tool defaults.
optional uint32 default_max_output_bytes = 1;
/// Per-tool max output byte overrides.
/// Keys are canonical tool names (e.g., "run_terminal_cmd", "grep").
map<string, uint32> per_tool_max_output_bytes = 2;
/// Deprecated and ignored: read_file no longer clips lines (clipping
/// silently corrupts single-line files; the whole-read token cap bounds
/// output instead). Field number kept for wire compatibility.
optional uint32 max_chars_per_line = 3 [deprecated = true];
/// Max lines to read for read_file. Default: 1000.
optional uint32 max_lines_read = 4;
}
/// Output format preference - affects output_json structure.
/// Only DEFAULT and CONCISE are supported; other values are treated as DEFAULT.
enum OutputFormat {
OUTPUT_FORMAT_UNSPECIFIED = 0;
/// Standard output with typical detail level
OUTPUT_FORMAT_DEFAULT = 1;
/// Minimal output with only essential fields (saves tokens)
OUTPUT_FORMAT_CONCISE = 2;
// Fields 3-4 removed (RAW, STRUCTURED were never implemented).
reserved 3, 4;
}
message ExecuteToolResponse {
/// Unique call ID for tracing
string call_id = 1;
/// Tool execution result
oneof result {
ToolSuccess success = 2;
ToolError error = 3;
}
/// Execution metadata
ExecutionMetadata metadata = 4;
}
message ToolSuccess {
// Field 1 removed: output_schema_json moved to ToolInfo.output_schema_json.
reserved 1;
/// Structured output as JSON (conforms to the tool's output schema)
string output_json = 2;
/// Human-readable summary for prompt injection
string prompt_text = 3;
// Field 4 removed: was follow_up_messages, the tool follow-up mechanism.
reserved 4;
reserved "follow_up_messages";
}
message ToolError {
/// Error code for machine processing
ErrorCode code = 1;
/// Human-readable error message
string message = 2;
/// Detailed error context as JSON
optional string details_json = 3;
/// Whether this error is retryable
bool retryable = 4;
/// Suggested fix or next action (for LLM consumption)
optional string suggestion = 5;
/// Related file path (if applicable)
optional string file_path = 6;
/// Line number (if applicable)
optional int32 line_number = 7;
}
enum ErrorCode {
ERROR_CODE_UNSPECIFIED = 0;
// Input/validation errors (4xx-like)
ERROR_CODE_INVALID_INPUT = 100;
ERROR_CODE_MISSING_REQUIRED_FIELD = 101;
ERROR_CODE_INVALID_TOOL_NAME = 102;
ERROR_CODE_TOOL_DISABLED = 103;
ERROR_CODE_TOOL_NOT_FOUND = 104;
reserved 106; // was TASK_NOT_FOUND, now handled by tool layer
// File operation errors
ERROR_CODE_FILE_NOT_FOUND = 200;
ERROR_CODE_FILE_NOT_READ = 201;
ERROR_CODE_FILE_EXTERNALLY_MODIFIED = 202;
ERROR_CODE_FILE_ALREADY_EXISTS = 203;
ERROR_CODE_PERMISSION_DENIED = 204;
ERROR_CODE_MULTIPLE_MATCHES = 205;
ERROR_CODE_NO_MATCHES = 206;
// Execution errors (5xx-like)
ERROR_CODE_EXECUTION_FAILED = 300;
ERROR_CODE_TIMEOUT = 301;
ERROR_CODE_CANCELLED = 302;
ERROR_CODE_RATE_LIMITED = 303;
// Internal errors
ERROR_CODE_INTERNAL = 500;
ERROR_CODE_NOT_IMPLEMENTED = 501;
}
message ExecutionMetadata {
/// Time taken to execute in milliseconds
uint64 duration_ms = 1;
/// Tokens estimated in output (for LLM context management)
int32 estimated_tokens = 2;
/// Whether output was truncated
bool truncated = 3;
/// Tool version that executed
string tool_version = 4;
/// Resolved behavior contract version for the tool that executed
/// (e.g. "current", "legacy-0.4.10"). Empty for unmanaged tools.
string contract_version = 5;
}
/// Streaming output chunk
message ToolStreamChunk {
string call_id = 1;
oneof chunk {
/// Incremental data chunk (prompt_text or output_json fragment)
StreamDataChunk data = 2;
/// Final result (always the last chunk metadata only, no payload)
StreamFinalResult final_result = 3;
}
}
/// What kind of data this chunk carries.
enum StreamDataKind {
STREAM_DATA_KIND_UNSPECIFIED = 0;
/// Fragment of the human-readable prompt_text string.
STREAM_DATA_KIND_PROMPT_TEXT = 1;
/// Fragment of the structured output_json string.
STREAM_DATA_KIND_OUTPUT_JSON = 2;
}
/// A single chunk of streamed data (either prompt_text or output_json).
message StreamDataChunk {
/// Which field this chunk belongs to.
StreamDataKind kind = 1;
/// Raw bytes of the chunk (UTF-8 encoded string fragment).
bytes data = 2;
/// Byte offset of this chunk within the complete field.
/// First chunk starts at 0. Subsequent chunks continue from
/// previous offset + len(data).
uint64 offset = 3;
}
/// Final result sent as the last streaming chunk.
/// Contains only metadata and error information the actual payload
/// (prompt_text, output_json) has already been streamed via StreamDataChunk.
message StreamFinalResult {
/// Execution metadata (timing, token estimates, truncation).
ExecutionMetadata metadata = 1;
/// If the tool execution failed, this contains the error.
/// When present, any previously streamed data chunks should be discarded.
optional ToolError error = 2;
/// Total size in bytes of all prompt_text chunks combined.
uint64 prompt_text_size = 3;
/// Total size in bytes of all output_json chunks combined.
uint64 output_json_size = 4;
// Field 5 removed: was follow_up_messages, the tool follow-up mechanism.
reserved 5;
reserved "follow_up_messages";
}
// ============================================================================
// TOOL DISCOVERY MESSAGES
// ============================================================================
message ListToolsRequest {
/// Filter by tool category
optional ToolCategory category = 1;
/// Include disabled tools
bool include_disabled = 2;
/// Include full schemas (can be large)
bool include_schemas = 3;
}
enum ToolCategory {
TOOL_CATEGORY_UNSPECIFIED = 0;
TOOL_CATEGORY_FILE = 1;
TOOL_CATEGORY_SEARCH = 2;
TOOL_CATEGORY_SHELL = 3;
TOOL_CATEGORY_WORKFLOW = 4;
TOOL_CATEGORY_EXTERNAL = 5;
TOOL_CATEGORY_CUSTOM = 6;
}
message ListToolsResponse {
repeated ToolInfo tools = 1;
int32 total_count = 2;
int32 enabled_count = 3;
}
message GetToolInfoRequest {
string tool_name = 1;
}
message ToolInfo {
/// Unique tool identifier
string name = 1;
/// Tool display name
string display_name = 2;
/// Full description (for LLM system prompt)
string description = 3;
/// Short description (for UI)
optional string short_description = 4;
/// Tool category
ToolCategory category = 5;
/// JSON Schema for input parameters (as JSON string)
string input_schema_json = 6;
/// Current state
bool enabled = 7;
/// Tool capabilities/features
ToolCapabilities capabilities = 8;
/// Version info
string version = 9;
/// Whether this is a built-in or custom tool
ToolSource source = 10;
/// Reserved for removed config_schema_json
reserved 11;
/// Tool-specific options schema (JSON Schema as JSON string)
/// Defines what options this tool accepts
optional string options_schema_json = 12;
/// Default values for tool options (as JSON string)
optional string default_options_json = 13;
/// Current effective options (defaults merged with configured, as JSON string)
optional string current_options_json = 14;
/// Supported output formats and what each includes.
/// Each OutputFormat value should appear at most once.
/// Server will reject duplicates during tool registration.
repeated OutputFormatSpec output_formats = 15;
/// All available output fields with descriptions.
/// Each field name should be unique.
/// Server will reject duplicates during tool registration.
repeated OutputFieldSpec output_fields = 16;
/// JSON Schema describing the structure of output_json returned by ExecuteTool.
/// Generated from the tool's output types via schemars.
string output_schema_json = 17;
/// Resolved behavior contract version for this tool (e.g. "current",
/// "legacy-0.4.10"). Empty for unmanaged tools.
string contract_version = 18;
/// Tool namespace (e.g. "GrokBuild", "Codex", "OpenCode").
/// Populated by ListTools pre-finalization from the fully-qualified
/// registry key (e.g. "GrokBuild:grep" namespace="GrokBuild").
string namespace = 19;
}
/// Describes an output format supported by a tool
message OutputFormatSpec {
/// Which format this describes
OutputFormat format = 1;
/// Human-readable description of this format
string description = 2;
/// Field names included by default in this format
repeated string default_fields = 3;
}
/// Describes an available output field
message OutputFieldSpec {
/// Field name (used in include_fields/exclude_fields)
string name = 1;
/// Human-readable description
string description = 2;
/// JSON type: "string", "number", "boolean", "array", "object"
string json_type = 3;
/// Example value (as JSON string)
optional string example_json = 4;
/// Which formats include this field by default
repeated OutputFormat included_in = 5;
/// If true, this field can be large (useful for token management)
bool potentially_large = 6;
}
message ToolCapabilities {
/// Can run in background
bool supports_background = 1;
/// Can stream output
bool supports_streaming = 2;
/// Reserved for future session support
reserved 3;
/// Has side effects (modifies files/system)
bool has_side_effects = 4;
/// Can be cancelled mid-execution
bool supports_cancellation = 5;
/// Supports timeout configuration
bool supports_timeout = 6;
/// Custom capability flags
repeated string custom_capabilities = 10;
}
enum ToolSource {
TOOL_SOURCE_UNSPECIFIED = 0;
TOOL_SOURCE_BUILTIN = 1;
TOOL_SOURCE_MCP = 2;
TOOL_SOURCE_CUSTOM = 3;
TOOL_SOURCE_SKILL = 4;
}
// ============================================================================
// DYNAMIC TOOL MANAGEMENT MESSAGES
// ============================================================================
message EnableToolRequest {
string tool_name = 1;
}
message EnableToolResponse {
bool success = 1;
string message = 2;
}
message DisableToolRequest {
string tool_name = 1;
}
message DisableToolResponse {
bool success = 1;
string message = 2;
}
// ============================================================================
// TOOL OPTIONS MESSAGES
// ============================================================================
message SetToolOptionsRequest {
string tool_name = 1;
/// Options to set (as JSON string)
/// These are merged with existing options unless replace=true
string options_json = 2;
/// If true, replace all options instead of merging
bool replace = 3;
}
message SetToolOptionsResponse {
bool success = 1;
string message = 2;
/// The effective options after setting (as JSON string)
string effective_options_json = 3;
}
message GetToolOptionsRequest {
string tool_name = 1;
/// If true, include the options schema in the response
bool include_schema = 2;
}
message GetToolOptionsResponse {
string tool_name = 1;
/// Current effective options (defaults + configured, as JSON string)
string effective_options_json = 2;
/// Default options for this tool (as JSON string)
string default_options_json = 3;
/// User-configured options that override defaults (as JSON string)
string configured_options_json = 4;
/// JSON Schema for options (if include_schema was true)
optional string options_schema_json = 5;
}
message ResetToolOptionsRequest {
string tool_name = 1;
/// If provided, only reset these specific option keys
/// If empty, reset all options to defaults
repeated string keys_to_reset = 2;
}
message ResetToolOptionsResponse {
bool success = 1;
string message = 2;
/// The effective options after reset (as JSON string)
string effective_options_json = 3;
}
// ============================================================================
// TOOL NAME OVERRIDE MESSAGES
// ============================================================================
message SetToolOverrideRequest {
/// The canonical tool name to set overrides for.
string tool_name = 1;
/// The model-facing tool name. If empty, the canonical name is used
/// (clears any existing tool name override for this tool).
string tool_name_for_model = 2;
/// Parameter name overrides (canonical param model-facing param).
/// Only include params that need overriding unlisted params keep
/// their canonical names. Empty map clears param overrides.
map<string, string> param_name_overrides_for_model = 3;
}
message SetToolOverrideResponse {
bool success = 1;
string message = 2;
}
message ClearToolOverrideRequest {
/// The canonical tool name to clear all overrides for.
string tool_name = 1;
}
message ClearToolOverrideResponse {
bool success = 1;
string message = 2;
}
// ============================================================================
// SYSTEM REMINDERS MESSAGES
// ============================================================================
message SetSystemRemindersRequest {
/// Whether system reminders should be enabled.
bool enabled = 1;
}
message SetSystemRemindersResponse {
bool success = 1;
string message = 2;
/// The effective state after the change.
bool enabled = 3;
}
message GetSystemRemindersRequest {}
message GetSystemRemindersResponse {
/// Whether system reminders are currently enabled.
bool enabled = 1;
}
// ============================================================================
// TRUNCATION CONFIG MESSAGES
// ============================================================================
message SetTruncationConfigRequest {
/// The truncation configuration to apply for the session.
TruncationConfig config = 1;
}
message SetTruncationConfigResponse {}
message GetTruncationConfigRequest {}
message GetTruncationConfigResponse {
/// The current effective truncation configuration.
TruncationConfig config = 1;
}
// ============================================================================
// AGENT MESSAGES
// ============================================================================
message GetSystemPromptRequest {
/// "full" or "compact". Defaults to "full" if empty.
string variant = 1;
}
message GetSystemPromptResponse {
/// The rendered system prompt.
string system_prompt = 1;
/// "extend" or "full" the prompt mode used.
string prompt_mode = 2;
}
message GetAgentInfoRequest {}
message GetAgentInfoResponse {
string name = 1;
string description = 2;
repeated string tools = 3;
repeated string disallowed_tools = 4;
string permission_mode = 5;
string output_format = 6;
/// Completion requirement (if set).
AgentCompletionRequirement completion_requirement = 7;
/// Per-tool execution config.
map<string, AgentToolExecConfig> tool_config = 8;
}
/// Completion requirement from the agent definition.
message AgentCompletionRequirement {
string tool = 1;
string reminder = 2;
}
/// Per-tool execution config from the agent definition.
message AgentToolExecConfig {
AgentToolRetryConfig retry = 1;
}
/// Retry config for a single tool.
message AgentToolRetryConfig {
uint32 max_retries = 1;
uint64 base_delay_ms = 2;
uint64 max_delay_ms = 3;
}
message GetCompletionStateRequest {}
message GetCompletionStateResponse {
/// Whether a completion requirement exists for this agent.
bool has_requirement = 1;
/// The tool that must be called.
string required_tool = 2;
/// Whether the tool was called this turn.
bool completed = 3;
}
message ResetCompletionStateRequest {}
message ResetCompletionStateResponse {}
message FinalizeAgentRequest {}
message FinalizeAgentResponse {
bool success = 1;
/// The finalized system prompt (same as GetSystemPrompt would return).
string system_prompt = 2;
}
// ============================================================================
// TOOL STATE MESSAGES
// ============================================================================
message GetToolStateRequest {}
message GetToolStateResponse {
/// The serialized tool state as a JSON string.
///
/// Shape: `{ "params": { ... }, "state": { ... } }`.
/// Contains all registered (serializable) Resources file read tracker,
/// todo state, todo nudge counters, per-tool params, etc.
/// Ephemeral resources (cwd, terminal, notification handle) are excluded.
string state_json = 1;
}

View file

@ -0,0 +1,257 @@
//! Validation of [`ToolConfigEntry`](crate::ToolConfigEntry) fields,
//! shared so the backend's save-time check cannot drift from what the
//! tools server enforces at finalize/bind. Errors carry the offending input
//! so callers can render gRPC violations without re-parsing.
use serde_json::{Map, Value};
use xai_tool_protocol::ToolId;
/// Why a [`ToolConfigEntry`](crate::ToolConfigEntry) is invalid.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ToolConfigEntryErrorKind {
/// Not valid JSON. Includes an explicitly-set empty string: proto3
/// `optional` tracks presence, so `Some("")` is rejected, not unset.
ParamsJsonParse { error: String, raw: String },
/// Valid JSON but not an object.
ParamsJsonNotObject { value: Value },
/// `name_override` is not a valid `ToolId` (charset/length contract).
NameOverrideInvalid { name: String, error: String },
}
/// Validation error for one entry in a tool-config list.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolConfigEntryError {
pub index: usize,
pub tool_id: String,
pub kind: ToolConfigEntryErrorKind,
}
impl ToolConfigEntryError {
/// Request field path of the failing field, e.g. `tools[3].params_json`.
pub fn field_path(&self) -> String {
match self.kind {
ToolConfigEntryErrorKind::ParamsJsonParse { .. }
| ToolConfigEntryErrorKind::ParamsJsonNotObject { .. } => {
format!("tools[{}].params_json", self.index)
}
ToolConfigEntryErrorKind::NameOverrideInvalid { .. } => {
format!("tools[{}].name_override", self.index)
}
}
}
}
impl std::fmt::Display for ToolConfigEntryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.kind {
ToolConfigEntryErrorKind::ParamsJsonParse { error, .. } => write!(
f,
"{}: {} failed to parse JSON: {error}",
self.tool_id,
self.field_path()
),
ToolConfigEntryErrorKind::ParamsJsonNotObject { .. } => write!(
f,
"{}: {} must be a JSON object",
self.tool_id,
self.field_path()
),
ToolConfigEntryErrorKind::NameOverrideInvalid { name, error } => write!(
f,
"{}: {} is not a valid tool name ({name:?}): {error}",
self.tool_id,
self.field_path()
),
}
}
}
impl std::error::Error for ToolConfigEntryError {}
/// Parse and validate a `params_json`, returning the decoded object (or
/// `None` when unset). `index`/`tool_id` are only used for error reporting.
pub fn parse_params_json(
index: usize,
tool_id: &str,
params_json: Option<&str>,
) -> Result<Option<Map<String, Value>>, ToolConfigEntryError> {
let Some(raw) = params_json else {
return Ok(None);
};
let value: Value = serde_json::from_str(raw).map_err(|err| ToolConfigEntryError {
index,
tool_id: tool_id.to_owned(),
kind: ToolConfigEntryErrorKind::ParamsJsonParse {
error: err.to_string(),
raw: raw.to_owned(),
},
})?;
match value {
Value::Object(object) => Ok(Some(object)),
other => Err(ToolConfigEntryError {
index,
tool_id: tool_id.to_owned(),
kind: ToolConfigEntryErrorKind::ParamsJsonNotObject { value: other },
}),
}
}
/// Validates a `name_override` against the `ToolId` charset/length contract,
/// mirroring [`parse_params_json`] as the shared source of truth.
pub fn validate_name_override(
index: usize,
tool_id: &str,
name_override: Option<&str>,
) -> Result<(), ToolConfigEntryError> {
let Some(name) = name_override else {
return Ok(());
};
ToolId::new(name).map_err(|err| ToolConfigEntryError {
index,
tool_id: tool_id.to_owned(),
kind: ToolConfigEntryErrorKind::NameOverrideInvalid {
name: name.to_owned(),
error: err.to_string(),
},
})?;
Ok(())
}
/// Returns the first entry whose `id` is not in `allowed_ids`, as
/// `(index, id)`, or `None` when all ids are allowed.
///
/// Pure so backend save-time validation and any future consumer share one rule.
pub fn first_unknown_tool_id<'a>(
entries: &'a [crate::ToolConfigEntry],
allowed_ids: &std::collections::HashSet<String>,
) -> Option<(usize, &'a str)> {
entries
.iter()
.enumerate()
.find(|(_, entry)| !allowed_ids.contains(&entry.id))
.map(|(index, entry)| (index, entry.id.as_str()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unset_params_is_ok_none() {
assert_eq!(parse_params_json(0, "GrokBuild:grep", None), Ok(None));
}
#[test]
fn valid_object_is_returned() {
let parsed = parse_params_json(0, "GrokBuild:grep", Some(r#"{"max_results":50}"#)).unwrap();
assert_eq!(
parsed,
Some(
serde_json::json!({"max_results": 50})
.as_object()
.unwrap()
.clone()
)
);
}
#[test]
fn empty_string_is_a_parse_error() {
let err = parse_params_json(3, "GrokBuild:grep", Some("")).unwrap_err();
assert_eq!(err.index, 3);
assert_eq!(err.field_path(), "tools[3].params_json");
assert!(matches!(
err.kind,
ToolConfigEntryErrorKind::ParamsJsonParse { .. }
));
}
#[test]
fn invalid_json_is_a_parse_error() {
let err = parse_params_json(0, "t", Some("{not json")).unwrap_err();
assert!(matches!(
err.kind,
ToolConfigEntryErrorKind::ParamsJsonParse { raw, .. } if raw == "{not json"
));
}
#[test]
fn non_object_json_is_rejected() {
let err = parse_params_json(1, "t", Some("[1,2,3]")).unwrap_err();
assert!(matches!(
err.kind,
ToolConfigEntryErrorKind::ParamsJsonNotObject {
value: Value::Array(_)
}
));
}
#[test]
fn name_override_unset_or_valid_is_ok() {
assert_eq!(validate_name_override(0, "GrokBuild:grep", None), Ok(()));
for name in ["search", "GrokBuild:grep", "a-b_C9"] {
assert_eq!(
validate_name_override(0, "GrokBuild:grep", Some(name)),
Ok(()),
"name={name:?}"
);
}
}
#[test]
fn name_override_outside_charset_is_rejected() {
for name in ["has space", "", "a:b:c", "dot.name"] {
let err = validate_name_override(2, "GrokBuild:grep", Some(name)).unwrap_err();
assert_eq!(err.index, 2, "name={name:?}");
assert_eq!(err.field_path(), "tools[2].name_override");
assert!(
matches!(
&err.kind,
ToolConfigEntryErrorKind::NameOverrideInvalid { name: n, .. } if n == name
),
"name={name:?} kind={:?}",
err.kind
);
}
}
fn entry(id: &str) -> crate::ToolConfigEntry {
crate::ToolConfigEntry {
id: id.to_owned(),
..Default::default()
}
}
fn allowed(ids: &[&str]) -> std::collections::HashSet<String> {
ids.iter().map(|s| (*s).to_owned()).collect()
}
#[test]
fn all_ids_present_returns_none() {
let entries = [entry("GrokBuild:grep"), entry("GrokBuild:read_file")];
let allowed = allowed(&["GrokBuild:grep", "GrokBuild:read_file", "GrokBuild:bash"]);
assert_eq!(first_unknown_tool_id(&entries, &allowed), None);
}
#[test]
fn empty_entries_returns_none() {
assert_eq!(
first_unknown_tool_id(&[], &allowed(&["GrokBuild:grep"])),
None
);
}
#[test]
fn first_unknown_id_is_returned_with_index() {
let entries = [
entry("GrokBuild:grep"),
entry("GrokBuild:nonexistent"),
entry("GrokBuild:also_missing"),
];
let allowed = allowed(&["GrokBuild:grep"]);
assert_eq!(
first_unknown_tool_id(&entries, &allowed),
Some((1, "GrokBuild:nonexistent"))
);
}
}

View file

@ -0,0 +1,139 @@
//! Shared API definitions for Grok tools: protobuf types, config validation,
//! and canonical slash-command wording.
//!
//! Used by both the tools library and the gRPC server, and by host services
//! that must not depend on the tools implementation crate.
#![allow(clippy::derive_partial_eq_without_eq)]
/// Generated protobuf types.
pub mod pb {
include!(concat!(env!("OUT_DIR"), "/xai.grok.tools.v1.rs"));
}
pub mod config_validation;
pub mod slash_commands;
// Re-export commonly used types at the crate root for convenience
pub use pb::{
// Agent types
AgentCompletionRequirement,
AgentToolExecConfig,
AgentToolRetryConfig,
// Request/response types
ClearToolOverrideRequest,
ClearToolOverrideResponse,
DisableToolRequest,
DisableToolResponse,
EnableToolRequest,
EnableToolResponse,
// Enums
ErrorCode,
ExecuteToolRequest,
ExecuteToolResponse,
ExecutionMetadata,
ExecutionOptions,
FinalizeAgentRequest,
FinalizeAgentResponse,
FinalizeConfigValidationDetails,
FinalizeConfigViolation,
// Tool server config (finalize-time)
FinalizeToolServerConfigRequest,
FinalizeToolServerConfigResponse,
GetAgentInfoRequest,
GetAgentInfoResponse,
GetCompletionStateRequest,
GetCompletionStateResponse,
GetSystemPromptRequest,
GetSystemPromptResponse,
GetSystemRemindersRequest,
GetSystemRemindersResponse,
GetToolInfoRequest,
GetToolOptionsRequest,
GetToolOptionsResponse,
// Tool state
GetToolStateRequest,
GetToolStateResponse,
// Truncation config
GetTruncationConfigRequest,
GetTruncationConfigResponse,
ListToolsRequest,
ListToolsResponse,
// Output format specs
OutputFieldSpec,
OutputFormat,
OutputFormatSpec,
ResetCompletionStateRequest,
ResetCompletionStateResponse,
ResetToolOptionsRequest,
ResetToolOptionsResponse,
SetSystemRemindersRequest,
SetSystemRemindersResponse,
SetToolOptionsRequest,
SetToolOptionsResponse,
SetToolOverrideRequest,
SetToolOverrideResponse,
SetTruncationConfigRequest,
SetTruncationConfigResponse,
// Streaming types
StreamDataChunk,
StreamDataKind,
StreamFinalResult,
// Capability/metadata types
ToolCapabilities,
ToolCategory,
// Per-tool config entry
ToolConfigEntry,
ToolError,
ToolInfo,
ToolSource,
ToolStreamChunk,
ToolSuccess,
TruncationConfig,
// Version lifecycle warnings
VersionWarning,
};
/// Default client-facing tool name derived from a namespaced tool id.
///
/// Tool ids are colon-separated `Namespace:tool` (e.g. `GrokBuild:grep`); the
/// default name is the segment after the FIRST colon, so an id with embedded
/// colons (`ns:a:b`) resolves to `a`. Ids without a colon are returned as-is.
///
/// This is the single source of truth shared by the tools server (which
/// advertises tools under this name unless `name_override` is set) and any
/// client that needs to predict the advertised name from a config entry
/// (e.g. prompt tool selection in a downstream service). Keeping both sides on
/// this helper prevents a silent desync that would drop tools from prompts.
pub fn default_client_name(id: &str) -> &str {
id.split(':').nth(1).unwrap_or(id)
}
/// Convert ToolCategory enum to a string representation.
impl ToolCategory {
/// Get the string representation of the category.
pub fn as_str(&self) -> &'static str {
match self {
Self::Unspecified => "unspecified",
Self::File => "file",
Self::Search => "search",
Self::Shell => "shell",
Self::Workflow => "workflow",
Self::External => "external",
Self::Custom => "custom",
}
}
}
#[cfg(test)]
mod default_client_name_tests {
use super::default_client_name;
#[test]
fn pins_first_colon_derivation() {
assert_eq!(default_client_name("GrokBuild:grep"), "grep");
assert_eq!(default_client_name("ns:a:b"), "a");
assert_eq!(default_client_name("bare"), "bare");
assert_eq!(default_client_name(""), "");
}
}

View file

@ -0,0 +1,217 @@
//! Canonical slash-command wording (`/loop`, `/imagine`, `/imagine-video`, `/goal`),
//! shared by every front-end (Grok Build shell/pager and other hosts) so
//! expansions cannot drift.
/// Canonical tool name advertised by the scheduler create tool. Gating code
/// (shell `CommandAvailability`, pager `required_tools`, host command lists)
/// keys `/loop` availability on this name.
pub const SCHEDULER_CREATE_TOOL_NAME: &str = "scheduler_create";
/// Usage hint shown when `/loop` is invoked with no arguments.
pub fn loop_usage_message() -> &'static str {
"Usage: /loop [interval] <prompt>\n\
Example: /loop 30m check deploy status\n\
Example: /loop check deploy status every hour\n\n\
Tell me how often it should run (e.g. 30m, 1 hour, every 2 days)."
}
/// Build the model instruction that `/loop` expands into for `args`.
///
/// The model, not brittle host parsing, turns the request into the
/// `scheduler_create` interval, accepting every natural phrasing and erroring
/// on bad input rather than silently defaulting. See [`loop_usage_message`].
pub fn loop_schedule_instruction(args: &str) -> String {
format!(
"# /loop -- schedule a recurring prompt\n\n\
Parse the input below into an interval and a prompt, then schedule it with scheduler_create.\n\n\
## Deriving the interval\n\
Read how often to run from the user's request however they phrase it and convert it\n\
to a compact `<number><unit>` string, where unit is one of `s` (seconds), `m` (minutes),\n\
`h` (hours), or `d` (days). The interval may appear at the start or end of the request;\n\
extract it and use the remaining text as the prompt.\n\n\
The minimum interval is 60 seconds; shorter values are raised to 60s, so tell the user if that applies.\n\n\
If the request contains no interval at all, ask the user how often it should run before\n\
scheduling. Do NOT invent or assume a default interval.\n\n\
## Action\n\
1. Call scheduler_create with: interval (the compact string you derived), prompt,\n\
recurring: true, fire_immediately: true. If the interval is unparseable, the tool\n\
returns an error fix the interval string rather than guessing.\n\
2. Confirm: what's scheduled, the cadence, that it auto-expires after 7 days,\n\
and that they can cancel with scheduler_delete (include the job ID).\n\
3. Do NOT execute the prompt inline. The scheduler will fire it immediately.\n\n\
## Input\n\
{args}"
)
}
/// Canonical name of the image generation tool; gates `/imagine`.
pub const IMAGE_GEN_TOOL_NAME: &str = "image_gen";
/// Advertised name of the /imagine command.
pub const IMAGINE_COMMAND_NAME: &str = "imagine";
/// Canonical name of the image-to-video tool; gates `/imagine-video`.
pub const IMAGE_TO_VIDEO_TOOL_NAME: &str = "image_to_video";
/// Advertised name of the /imagine-video command.
pub const IMAGINE_VIDEO_COMMAND_NAME: &str = "imagine-video";
/// Usage hint shown when `/imagine` is invoked with no arguments.
pub fn imagine_usage_message() -> &'static str {
"Usage: /imagine <description>\n\
Provide a text description to generate an image."
}
/// Build the model instruction that `/imagine` expands into for `prompt`.
pub fn imagine_instruction(prompt: &str) -> String {
format!(
"Call the image_gen tool immediately, passing the user's prompt below \
verbatim do not rewrite, embellish, or expand it. \
After the tool completes, briefly acknowledge and mention \
where the image was saved.\n\n\
Prompt: {prompt}"
)
}
/// Usage hint shown when `/imagine-video` is invoked with no arguments.
pub fn imagine_video_usage_message() -> &'static str {
"Usage: /imagine-video <description>\n\
Provide a text description to generate a video."
}
/// Build the model instruction that `/imagine-video` expands into for `prompt`.
pub fn imagine_video_instruction(prompt: &str) -> String {
format!(
"{IMAGINE_VIDEO_SKILL}\n\n\
User prompt: {prompt}"
)
}
/// Video workflow guidance injected by `/imagine-video`.
const IMAGINE_VIDEO_SKILL: &str = "\
# Imagine Video
Video starts from an image there is no text-to-video tool. \
Default to `image_to_video`; use `reference_to_video` only when the user \
explicitly asks for it or a shot genuinely needs multiple reference images.
## Default: single clip
Unless the user asks for a long video, multiple scenes, or a multi-shot sequence, \
generate **one** video:
1. Create a source image with `image_gen` that stages the first frame \
(composition, subject, lighting).
2. Call `image_to_video` with that image and a short prompt describing the motion \
or camera move (12 sentences, present tense).
3. After the tool completes, mention the saved file path so the user can find it.
## Longer / multi-shot videos
When the user requests a longer video, multiple scenes, or a narrative sequence:
1. **Plan the story as shots** break the idea into distinct shots, one beat each.
2. **Favor frequent, short shots** prefer more 6s clips over fewer long ones; more cuts keep it dynamic.
3. **Create each shot's source image** with `image_gen` (or `image_edit` to combine references), keeping characters and settings consistent across shots.
4. **Animate each shot with `image_to_video`** the source image becomes frame 1.
5. **Assemble with FFmpeg** using stream copy (`ffmpeg -f concat ... -c copy` never re-encode). \
Keep every shot at the same resolution and frame rate so the concat works. \
After assembly, mention the final output path.
## Shot guidance
- **Prompt-craft:** one short, vivid moment in present tense with a clear camera movement, in 12 sentences.
- **Minimal but interesting:** one clear subject, one simple motion or camera move per shot. Avoid complex multi-action animation; make the shot compelling through composition, lighting, and a strong moment.
- **Complex source image?** Intricate frames (busy geometry, fine detail, heavy reflections) warp when animated. Keep the subject fixed and move only the camera (slow push-in, orbit, or parallax), or break into simpler shots. For new shots, generate a simpler, animation-friendly base image rather than animating a busy one.
- **`image_to_video` animates from frame 1** stage the first frame with `image_gen`/`image_edit` before animating.
- **Aspect ratio:** set it on the source image (`image_gen` `aspect_ratio`); don't re-crop an existing video.
- **Duration:** 6s or 10s only (prefer 6s); round to the nearest.
- **Real people:** reference-first drive the video from a verified reference image; never animate a named person without one.
- Don't loop the same clip unless asked.";
pub const UPDATE_GOAL_TOOL_NAME: &str = "update_goal";
pub const GOAL_COMMAND_NAME: &str = "goal";
/// Bare subcommand tokens reserved for goal lifecycle control rather than
/// being treated as an objective, matching the shell's /goal grammar.
pub const GOAL_RESERVED_SUBCOMMANDS: &[&str] = &["status", "pause", "resume", "clear", "edit"];
pub fn goal_usage_message() -> &'static str {
"Usage: /goal <objective>\n\
Set an objective to work toward until it is complete."
}
pub fn goal_instruction(objective: &str) -> String {
format!(
"# /goal -- pursue an objective\n\n\
A goal has been set: {objective}\n\n\
Work directly on this goal and carry it as far as you can. Deliver \
everything the user asked for yourself: no follow-up questions, no \
manual steps left for the user. If the conversation continues, keep \
pursuing the goal until it is complete.\n\n\
TRACKING: break the objective into concrete steps and track them \
(use your todo tool if one is available), marking each done as you \
finish it.\n\n\
VERIFY AS YOU GO: test each change on the real path before moving on. \
A completion claim must be backed by evidence produced in this \
session, not assumptions.\n\n\
Call update_goal(completed: true, message: \"summary\") ONLY when the \
goal is fully achieved. Call update_goal(blocked_reason: \"reason\") \
only when truly stuck after 3+ consecutive failed attempts at the \
same problem. Call update_goal(message: \"status note\") to log \
progress along the way. If update_goal returns an error, continue \
working the goal and report status in your reply instead.\n\n\
Start now."
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn imagine_instruction_carries_prompt_verbatim() {
let text = imagine_instruction("a golden sunset");
assert!(text.contains("a golden sunset"));
assert!(text.contains("image_gen"));
assert!(text.contains("verbatim"));
}
#[test]
fn imagine_video_instruction_carries_prompt_and_workflow() {
let text = imagine_video_instruction("a cat playing piano");
assert!(text.contains("a cat playing piano"));
assert!(text.contains("image_to_video"));
assert!(text.contains("FFmpeg"));
}
#[test]
fn instruction_carries_args_and_contract_tokens() {
let text = loop_schedule_instruction("every 30 minutes do x");
assert!(text.contains("every 30 minutes do x"));
assert!(text.contains("<number><unit>"));
assert!(text.contains("ask the user how often"));
assert!(!text.contains("10m"), "no host-side default interval");
}
#[test]
fn goal_instruction_carries_objective_and_contract_tokens() {
let text = goal_instruction("ship the widget");
assert!(text.contains("ship the widget"));
assert!(text.contains("update_goal(completed: true"));
assert!(text.contains("blocked_reason"));
assert!(text.contains("If update_goal returns an error"));
assert!(
!text.contains("system-reminder"),
"expansions ride as user messages and must not claim reminder authority"
);
assert!(goal_usage_message().contains("Usage: /goal"));
}
#[test]
fn usage_message_has_no_default_claim() {
assert!(loop_usage_message().contains("Usage: /loop"));
assert!(!loop_usage_message().contains("10m"));
}
}

View file

@ -0,0 +1,102 @@
//! Serde wire-shape pin tests.
//!
//! `ToolConfigEntry` is serialized into session-bind metadata and backend
//! JSONB config storage. These tests pin the exact JSON shape so a
//! field rename/retype in `grok-tools.proto` cannot silently break those
//! wire contracts (the producer and consumer live in separate services).
use xai_grok_tools_api::ToolConfigEntry;
fn full_entry() -> ToolConfigEntry {
ToolConfigEntry {
id: "GrokBuild:grep".to_owned(),
params_json: Some(r#"{"max_results":50}"#.to_owned()),
name_override: Some("search".to_owned()),
params_name_overrides: std::collections::HashMap::from([(
"pattern".to_owned(),
"query".to_owned(),
)]),
behavior_version: Some("legacy-0.4.10".to_owned()),
description_override: Some("Search the codebase".to_owned()),
}
}
#[test]
fn tool_config_entry_serializes_to_pinned_json_shape() {
let value = serde_json::to_value(full_entry()).expect("serialize");
assert_eq!(
value,
serde_json::json!({
"id": "GrokBuild:grep",
"params_json": "{\"max_results\":50}",
"name_override": "search",
"params_name_overrides": {"pattern": "query"},
"behavior_version": "legacy-0.4.10",
"description_override": "Search the codebase",
})
);
}
#[test]
fn tool_config_entry_round_trips() {
let entry = full_entry();
let json = serde_json::to_string(&entry).expect("serialize");
let back: ToolConfigEntry = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, entry);
}
#[test]
fn minimal_entry_deserializes_from_id_only() {
// Consumers must accept sparse payloads: optional fields absent, map empty.
let back: ToolConfigEntry =
serde_json::from_value(serde_json::json!({"id": "GrokBuild:read_file"}))
.expect("deserialize minimal");
assert_eq!(back.id, "GrokBuild:read_file");
assert_eq!(back.params_json, None);
assert_eq!(back.name_override, None);
assert!(back.params_name_overrides.is_empty());
assert_eq!(back.behavior_version, None);
assert_eq!(back.description_override, None);
}
#[test]
fn missing_id_fails_to_deserialize() {
// `id` is the only required field: a payload without it must be rejected
// instead of silently deserializing with an empty id.
let result: Result<ToolConfigEntry, _> =
serde_json::from_value(serde_json::json!({"name_override": "search"}));
assert!(result.is_err(), "payload without `id` must be rejected");
}
#[test]
fn explicit_null_optional_fields_deserialize_as_none() {
// `#[serde(default)]` covers *absent* keys; explicit `null` is handled by
// the `Option` fields themselves. Pin that both shapes are accepted.
let back: ToolConfigEntry = serde_json::from_value(serde_json::json!({
"id": "GrokBuild:read_file",
"params_json": null,
"name_override": null,
"behavior_version": null,
"description_override": null,
}))
.expect("deserialize explicit nulls");
assert_eq!(back.params_json, None);
assert_eq!(back.name_override, None);
assert_eq!(back.behavior_version, None);
assert_eq!(back.description_override, None);
}
#[test]
fn explicit_null_map_is_rejected() {
// The map field is not `Option`-typed: `null` is not coerced to an empty
// map. Producers must omit the key or emit `{}`. Pin the rejection so a
// codegen change that silently starts accepting `null` is caught.
let result: Result<ToolConfigEntry, _> = serde_json::from_value(serde_json::json!({
"id": "GrokBuild:read_file",
"params_name_overrides": null,
}));
assert!(
result.is_err(),
"null params_name_overrides must be rejected (omit the key or send {{}})"
);
}