Publish harness and TUI open-source
initial sync from the monorepo
This commit is contained in:
commit
c68e39f604
2734 changed files with 1437016 additions and 0 deletions
25
crates/codegen/xai-grok-tools-api/Cargo.toml
Normal file
25
crates/codegen/xai-grok-tools-api/Cargo.toml
Normal 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
|
||||
34
crates/codegen/xai-grok-tools-api/build.rs
Normal file
34
crates/codegen/xai-grok-tools-api/build.rs
Normal 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();
|
||||
}
|
||||
953
crates/codegen/xai-grok-tools-api/proto/grok-tools.proto
Normal file
953
crates/codegen/xai-grok-tools-api/proto/grok-tools.proto
Normal 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;
|
||||
}
|
||||
257
crates/codegen/xai-grok-tools-api/src/config_validation.rs
Normal file
257
crates/codegen/xai-grok-tools-api/src/config_validation.rs
Normal 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"))
|
||||
);
|
||||
}
|
||||
}
|
||||
139
crates/codegen/xai-grok-tools-api/src/lib.rs
Normal file
139
crates/codegen/xai-grok-tools-api/src/lib.rs
Normal 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(""), "");
|
||||
}
|
||||
}
|
||||
217
crates/codegen/xai-grok-tools-api/src/slash_commands.rs
Normal file
217
crates/codegen/xai-grok-tools-api/src/slash_commands.rs
Normal 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 (1–2 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 1–2 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"));
|
||||
}
|
||||
}
|
||||
102
crates/codegen/xai-grok-tools-api/tests/wire_shape.rs
Normal file
102
crates/codegen/xai-grok-tools-api/tests/wire_shape.rs
Normal 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 {{}})"
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue