diff --git a/agent/context/JSAPI.md b/agent/context/JSAPI.md index ca62288a..c295f31b 100644 --- a/agent/context/JSAPI.md +++ b/agent/context/JSAPI.md @@ -47,13 +47,14 @@ interface Context { ### Send Messages -#### `ctx.Send(message): string` +#### `ctx.Send(message, blockId?): string` Sends a message to the client and automatically flushes the output. **Parameters:** - `message`: Message object or string +- `blockId`: String (optional) - Block ID to send this message in. If omitted, no block ID is assigned. **Returns:** @@ -66,6 +67,8 @@ interface Message { type: string; // Message type: "text", "tool", "image", etc. props: Record; // Message properties message_id?: string; // Optional message ID (auto-generated if omitted) + block_id?: string; // Optional block ID (auto-generated if omitted, has priority over blockId parameter) + thread_id?: string; // Optional thread ID (auto-set from current Stack if omitted) } ``` @@ -73,19 +76,33 @@ interface Message { ```javascript // Send text message (object format) and capture message ID -const messageId = ctx.Send({ +const message_id = ctx.Send({ type: "text", props: { content: "Hello, World!" }, }); -console.log("Sent message:", messageId); +console.log("Sent message:", message_id); -// Send text message (shorthand) -const textId = ctx.Send("Hello, World!"); +// Send text message (shorthand) - no block ID by default +const text_id = ctx.Send("Hello, World!"); -// Send tool message with custom ID -const toolId = ctx.Send({ +// Send multiple messages in the same block (same bubble/card in UI) +const block_id = ctx.BlockID(); // Generate block ID first +const msg1 = ctx.Send("Step 1: Analyzing...", block_id); +const msg2 = ctx.Send("Step 2: Processing...", block_id); +const msg3 = ctx.Send("Step 3: Complete!", block_id); + +// Specify block_id in message object (highest priority) +const msg4 = ctx.Send({ + type: "text", + props: { content: "In specific block" }, + block_id: "B2", // This takes priority over second parameter +}); + +// Send tool message with custom IDs +const tool_id = ctx.Send({ type: "tool", message_id: "custom-tool-msg-1", + block_id: "B_tools", props: { name: "calculator", result: { sum: 42 }, @@ -93,7 +110,7 @@ const toolId = ctx.Send({ }); // Send image message -const imageId = ctx.Send({ +const image_id = ctx.Send({ type: "image", props: { url: "https://example.com/image.png", @@ -102,12 +119,76 @@ const imageId = ctx.Send({ }); ``` +**Block Management:** + +```javascript +// Scenario 1: Simple messages without block grouping (most common) +function Next(ctx, response) { + // Each message is independent + const loading_id = ctx.Send({ + type: "loading", + props: { message: "Thinking..." } + }); + + // Call LLM... + const result = Process("llms.chat", {...}); + + // Replace loading with result + ctx.Replace(loading_id, { + type: "text", + props: { content: result.content } + }); +} + +// Scenario 2: Grouping messages in one block (special case) +function Create(ctx, messages) { + // Generate a block ID for grouping + const block_id = ctx.BlockID(); // "B1" + + ctx.Send("# Analysis Results", block_id); + ctx.Send("- Finding 1: ...", block_id); + ctx.Send("- Finding 2: ...", block_id); + ctx.Send("- Finding 3: ...", block_id); + + // All messages appear in the same card/bubble in the UI +} + +// Scenario 3: LLM response + follow-up card in same block +function Next(ctx, response) { + const block_id = ctx.BlockID(); + + // LLM response + const result = Process("llms.chat", {...}); + ctx.Send({ + type: "text", + props: { content: result.content }, + block_id: block_id + }); + + // Action card (grouped with LLM response) + ctx.Send({ + type: "card", + props: { + title: "Related Actions", + actions: [...] + }, + block_id: block_id + }); +} +``` + **Notes:** -- Message ID is automatically generated if not provided +- **Message ID** is automatically generated if not provided +- **Block ID** is NOT auto-generated by default (remains empty unless manually specified) + - Most messages don't need a Block ID (each message is independent) + - Only specify Block ID in special cases (e.g., grouping LLM output with a follow-up card) + - **Block ID priority**: message.block_id > blockId parameter > empty +- **Thread ID** is automatically set from Stack for non-root calls (nested agents) - Returns the message ID for reference in subsequent operations - Output is automatically flushed after sending - Throws exception on failure +- Delta operations (Replace, Append, Merge, Set) automatically inherit block_id and thread_id from the original message #### `ctx.Replace(messageId, message): string` @@ -126,13 +207,13 @@ Replaces an existing message with new content. This is useful for updating progr ```javascript // Send initial message -const msgId = ctx.Send("Processing..."); +const msg_id = ctx.Send("Processing..."); // Later, replace with updated content -ctx.Replace(msgId, "Processing complete!"); +ctx.Replace(msg_id, "Processing complete!"); // Replace with complex message -ctx.Replace(msgId, { +ctx.Replace(msg_id, { type: "text", props: { content: "Task finished", @@ -141,25 +222,25 @@ ctx.Replace(msgId, { }); // Replace with shorthand text -ctx.Replace(msgId, "Updated text content"); +ctx.Replace(msg_id, "Updated text content"); ``` **Use Cases:** ```javascript // Progress updates -const progressId = ctx.Send("Step 1/3: Starting..."); +const progress_id = ctx.Send("Step 1/3: Starting..."); // ... do work ... -ctx.Replace(progressId, "Step 2/3: Processing..."); +ctx.Replace(progress_id, "Step 2/3: Processing..."); // ... do more work ... -ctx.Replace(progressId, "Step 3/3: Finalizing..."); +ctx.Replace(progress_id, "Step 3/3: Finalizing..."); // ... finish ... -ctx.Replace(progressId, "Complete! ✓"); +ctx.Replace(progress_id, "Complete! ✓"); // Error correction -const msgId = ctx.Send("Found 5 results"); +const msg_id = ctx.Send("Found 5 results"); // Oops, counted wrong -ctx.Replace(msgId, "Found 8 results"); +ctx.Replace(msg_id, "Found 8 results"); ``` **Notes:** @@ -169,6 +250,359 @@ ctx.Replace(msgId, "Found 8 results"); - Output is automatically flushed after replacing - Throws exception on failure +#### `ctx.Append(messageId, content, path?): string` + +Appends content to an existing message. This is useful for streaming or incrementally building up message content. + +**Parameters:** + +- `messageId`: String - The ID of the message to append to +- `content`: Message object or string - The content to append +- `path`: String (optional) - The delta path to append to (e.g., "props.content", "props.data") + +**Returns:** + +- `string`: The message ID (same as the provided messageId) + +**Examples:** + +```javascript +// Send initial message +const msg_id = ctx.Send("Starting"); + +// Append more text (default path) +ctx.Append(msg_id, "... processing"); +ctx.Append(msg_id, "... done!"); +// Result: "Starting... processing... done!" + +// Append to specific path +const data_id = ctx.Send({ + type: "data", + props: { + content: "Item 1\n", + status: "loading", + }, +}); + +ctx.Append(data_id, "Item 2\n", "props.content"); +ctx.Append(data_id, "Item 3\n", "props.content"); +// Result: props.content = "Item 1\nItem 2\nItem 3\n" + +// Shorthand text append +ctx.Append(msg_id, " more text"); +``` + +**Use Cases:** + +```javascript +// Streaming text output +const stream_id = ctx.Send(""); +ctx.Append(stream_id, "The"); +ctx.Append(stream_id, " quick"); +ctx.Append(stream_id, " brown"); +ctx.Append(stream_id, " fox"); +// Final: "The quick brown fox" + +// Building a list incrementally +const list_id = ctx.Send({ + type: "list", + props: { items: [] }, +}); + +ctx.Append(list_id, { items: ["Item 1"] }, "props.items"); +ctx.Append(list_id, { items: ["Item 2"] }, "props.items"); +ctx.Append(list_id, { items: ["Item 3"] }, "props.items"); + +// Progress logs +const log_id = ctx.Send({ + type: "log", + props: { content: "Starting process\n" }, +}); +ctx.Append(log_id, "Step 1 complete\n", "props.content"); +ctx.Append(log_id, "Step 2 complete\n", "props.content"); +ctx.Append(log_id, "All done!\n", "props.content"); +``` + +**Notes:** + +- The message must exist (must have been sent previously) +- Uses delta append operation (adds to existing content, doesn't replace) +- If `path` is omitted, appends to the default content location +- Output is automatically flushed after appending +- Throws exception on failure +- BlockID and ThreadID are inherited from the original message + +#### `ctx.Merge(messageId, data, path?): string` + +Merges data into an existing message object. This is useful for updating multiple fields in an object without replacing the entire object. + +**Parameters:** + +- `messageId`: String - The ID of the message to merge into +- `data`: Object - The data to merge (should be an object) +- `path`: String (optional) - The delta path to merge into (e.g., "props", "props.metadata") + +**Returns:** + +- `string`: The message ID (same as the provided messageId) + +**Examples:** + +```javascript +// Send initial message with object data +const msg_id = ctx.Send({ + type: "status", + props: { + status: "running", + progress: 0, + started: true, + }, +}); + +// Merge updates into props (adds/updates fields, keeps others unchanged) +ctx.Merge(msg_id, { progress: 50 }, "props"); +// Result: props = { status: "running", progress: 50, started: true } + +ctx.Merge(msg_id, { progress: 100, status: "completed" }, "props"); +// Result: props = { status: "completed", progress: 100, started: true } + +// Merge into nested object +ctx.Merge( + msg_id, + { + metadata: { + duration: 1500, + items_processed: 42, + }, + }, + "props" +); +// Result: props.metadata is added/merged +``` + +**Use Cases:** + +```javascript +// Updating task progress +const task_id = ctx.Send({ + type: "task", + props: { + name: "Data Processing", + status: "pending", + progress: 0, + }, +}); + +ctx.Merge(task_id, { status: "running" }, "props"); +ctx.Merge(task_id, { progress: 25 }, "props"); +ctx.Merge(task_id, { progress: 50 }, "props"); +ctx.Merge(task_id, { progress: 100, status: "completed" }, "props"); + +// Building metadata incrementally +const data_id = ctx.Send({ + type: "data", + props: { content: "Result data" }, +}); + +ctx.Merge(data_id, { metadata: { source: "api" } }, "props"); +ctx.Merge(data_id, { metadata: { timestamp: Date.now() } }, "props"); +// metadata fields are merged together +``` + +**Notes:** + +- The message must exist (must have been sent previously) +- Uses delta merge operation (merges objects, doesn't replace) +- Only works with object data (for merging key-value pairs) +- Existing fields not in the merge data remain unchanged +- If `path` is omitted, merges into the default object location +- Output is automatically flushed after merging +- Throws exception on failure +- BlockID and ThreadID are inherited from the original message + +#### `ctx.Set(messageId, data, path): string` + +Sets a new field or value in an existing message. This is useful for adding new fields to a message structure. + +**Parameters:** + +- `messageId`: String - The ID of the message to set the field in +- `data`: Any - The value to set +- `path`: String (required) - The delta path where to set the value (e.g., "props.newField", "props.metadata.key") + +**Returns:** + +- `string`: The message ID (same as the provided messageId) + +**Examples:** + +```javascript +// Send initial message +const msg_id = ctx.Send({ + type: "result", + props: { + content: "Initial content", + }, +}); + +// Set a new field +ctx.Set(msg_id, "success", "props.status"); +// Result: props.status = "success" + +// Set a nested object +ctx.Set(msg_id, { duration: 1500, cached: true }, "props.metadata"); +// Result: props.metadata = { duration: 1500, cached: true } + +// Set array value +ctx.Set(msg_id, ["tag1", "tag2", "tag3"], "props.tags"); +// Result: props.tags = ["tag1", "tag2", "tag3"] +``` + +**Use Cases:** + +```javascript +// Adding computed metadata after initial send +const result_id = ctx.Send({ + type: "search_result", + props: { results: [...] } +}); + +ctx.Set(result_id, results.length, "props.count"); +ctx.Set(result_id, Date.now(), "props.timestamp"); +ctx.Set(result_id, "relevance", "props.sort_by"); + +// Conditionally adding fields +if (has_error) { + ctx.Set(msg_id, error_message, "props.error"); + ctx.Set(msg_id, "error", "props.status"); +} + +// Building complex nested structures +const doc_id = ctx.Send({ + type: "document", + props: { title: "My Document" } +}); + +ctx.Set(doc_id, { author: "John", date: "2024" }, "props.metadata"); +ctx.Set(doc_id, ["draft", "reviewed"], "props.tags"); +ctx.Set(doc_id, 3, "props.version"); +``` + +**Notes:** + +- The message must exist (must have been sent previously) +- Uses delta set operation (creates/sets new fields) +- The `path` parameter is **required** (must specify where to set the value) +- Creates the path if it doesn't exist +- Use for adding new fields or completely replacing a field's value +- For updating existing object fields, consider using `Merge` instead +- Output is automatically flushed after setting +- Throws exception on failure +- BlockID and ThreadID are inherited from the original message + +### ID Generators + +These methods generate unique IDs for manual message management. Useful when you need to specify IDs before sending messages or for advanced Block/Thread management. + +#### `ctx.MessageID(): string` + +Generates a unique message ID. + +**Returns:** + +- `string`: Message ID in format "M1", "M2", "M3"... + +**Example:** + +```javascript +// Generate IDs manually +const id_1 = ctx.MessageID(); // "M1" +const id_2 = ctx.MessageID(); // "M2" + +// Use custom ID +ctx.Send({ + type: "text", + message_id: id_1, + props: { content: "Hello" }, +}); +``` + +#### `ctx.BlockID(): string` + +Generates a unique block ID for grouping messages. + +**Returns:** + +- `string`: Block ID in format "B1", "B2", "B3"... + +**Example:** + +```javascript +// Generate block ID for grouping messages +const block_id = ctx.BlockID(); // "B1" + +// Send multiple messages in the same block +ctx.Send("Step 1: Analyzing...", block_id); +ctx.Send("Step 2: Processing...", block_id); +ctx.Send("Step 3: Complete!", block_id); + +// All three messages appear in the same card/bubble in UI +``` + +**Use Cases:** + +```javascript +// Scenario: LLM output + follow-up card in same block +const block_id = ctx.BlockID(); + +// LLM response +const llm_result = Process("llms.chat", {...}); +ctx.Send({ + type: "text", + props: { content: llm_result.content }, + block_id: block_id, +}); + +// Follow-up action card (grouped with LLM output) +ctx.Send({ + type: "card", + props: { + title: "Related Actions", + actions: [...] + }, + block_id: block_id, +}); +``` + +#### `ctx.ThreadID(): string` + +Generates a unique thread ID for concurrent operations. + +**Returns:** + +- `string`: Thread ID in format "T1", "T2", "T3"... + +**Example:** + +```javascript +// For advanced parallel processing scenarios +const thread_id = ctx.ThreadID(); // "T1" + +// Send messages in a specific thread +ctx.Send({ + type: "text", + props: { content: "Parallel task 1" }, + thread_id: thread_id, +}); +``` + +**Notes:** + +- IDs are generated sequentially within each context +- Each context has its own ID counter (starts from 1) +- IDs are guaranteed to be unique within the same request/stream +- ThreadID is usually auto-managed by Stack, manual generation is for advanced use cases + ### Resource Cleanup #### `ctx.Release()` @@ -216,7 +650,7 @@ interface TraceNodeOption { **Example:** ```javascript -const node = ctx.Trace.Add( +const search_node = ctx.Trace.Add( { query: "What is AI?" }, { label: "Search Query", @@ -247,7 +681,7 @@ interface ParallelInput { **Example:** ```javascript -const nodes = ctx.Trace.Parallel([ +const parallel_nodes = ctx.Trace.Parallel([ { input: { url: "https://api1.com" }, option: { @@ -294,8 +728,8 @@ ctx.Trace.Error("Operation failed", { error: "timeout" }); Sets the output data for a node. ```javascript -const node = ctx.Trace.Add({ query: "search" }, options); -node.SetOutput({ results: [...] }); +const search_node = ctx.Trace.Add({ query: "search" }, options); +search_node.SetOutput({ results: [...] }); ``` #### `node.SetMetadata(key, value)` @@ -303,8 +737,8 @@ node.SetOutput({ results: [...] }); Sets metadata for a node. ```javascript -node.SetMetadata("duration", 1500); -node.SetMetadata("cache_hit", true); +search_node.SetMetadata("duration", 1500); +search_node.SetMetadata("cache_hit", true); ``` #### `node.Complete(output?)` @@ -312,7 +746,7 @@ node.SetMetadata("cache_hit", true); Marks a node as completed (optionally with output). ```javascript -node.Complete({ status: "success", data: [...] }); +search_node.Complete({ status: "success", data: [...] }); ``` #### `node.Fail(error)` @@ -323,7 +757,7 @@ Marks a node as failed with an error. try { // Operation } catch (error) { - node.Fail(error); + search_node.Fail(error); } ``` @@ -334,8 +768,8 @@ try { Returns the root node of the trace tree. ```javascript -const root = ctx.Trace.GetRootNode(); -console.log(root.id, root.label); +const root_node = ctx.Trace.GetRootNode(); +console.log(root_node.id, root_node.label); ``` #### `ctx.Trace.GetNode(id)` @@ -343,7 +777,7 @@ console.log(root.id, root.label); Retrieves a specific node by ID. ```javascript -const node = ctx.Trace.GetNode("node-123"); +const target_node = ctx.Trace.GetNode("node-123"); ``` #### `ctx.Trace.GetCurrentNodes()` @@ -351,7 +785,7 @@ const node = ctx.Trace.GetNode("node-123"); Returns the current active nodes (may be multiple if in parallel state). ```javascript -const currentNodes = ctx.Trace.GetCurrentNodes(); +const current_nodes = ctx.Trace.GetCurrentNodes(); ``` ### Memory Space Operations @@ -361,7 +795,7 @@ const currentNodes = ctx.Trace.GetCurrentNodes(); Creates a memory space for storing key-value data. ```javascript -const space = ctx.Trace.CreateSpace({ +const memory_space = ctx.Trace.CreateSpace({ label: "Context Memory", type: "context", icon: "database", @@ -374,7 +808,7 @@ const space = ctx.Trace.CreateSpace({ Retrieves a memory space by ID. ```javascript -const space = ctx.Trace.GetSpace("context"); +const context_space = ctx.Trace.GetSpace("context"); ``` #### `ctx.Trace.HasSpace(id)` @@ -400,8 +834,8 @@ ctx.Trace.DeleteSpace("temp_storage"); Lists all memory spaces. ```javascript -const spaces = ctx.Trace.ListSpaces(); -spaces.forEach((space) => { +const all_spaces = ctx.Trace.ListSpaces(); +all_spaces.forEach((space) => { console.log(space.id, space.label); }); ``` @@ -417,7 +851,7 @@ The `ctx.MCP` object provides access to Model Context Protocol operations for in Lists available resources from an MCP client. ```javascript -const resources = ctx.MCP.ListResources("filesystem"); +const fs_resources = ctx.MCP.ListResources("filesystem"); ``` #### `ctx.MCP.ReadResource(client, uri)` @@ -425,7 +859,10 @@ const resources = ctx.MCP.ListResources("filesystem"); Reads a specific resource. ```javascript -const content = ctx.MCP.ReadResource("filesystem", "file:///path/to/file.txt"); +const file_content = ctx.MCP.ReadResource( + "filesystem", + "file:///path/to/file.txt" +); ``` ### Tool Operations @@ -435,7 +872,7 @@ const content = ctx.MCP.ReadResource("filesystem", "file:///path/to/file.txt"); Lists available tools from an MCP client. ```javascript -const tools = ctx.MCP.ListTools("toolkit"); +const available_tools = ctx.MCP.ListTools("toolkit"); ``` #### `ctx.MCP.CallTool(client, name, args)` @@ -443,7 +880,7 @@ const tools = ctx.MCP.ListTools("toolkit"); Calls a single tool. ```javascript -const result = ctx.MCP.CallTool("calculator", "add", { +const calc_result = ctx.MCP.CallTool("calculator", "add", { a: 10, b: 32, }); @@ -454,7 +891,7 @@ const result = ctx.MCP.CallTool("calculator", "add", { Calls multiple tools sequentially. ```javascript -const results = ctx.MCP.CallTools("toolkit", [ +const tool_results = ctx.MCP.CallTools("toolkit", [ { name: "tool1", args: { param: "value1" } }, { name: "tool2", args: { param: "value2" } }, ]); @@ -465,7 +902,7 @@ const results = ctx.MCP.CallTools("toolkit", [ Calls multiple tools in parallel. ```javascript -const results = ctx.MCP.CallToolsParallel("toolkit", [ +const parallel_results = ctx.MCP.CallToolsParallel("toolkit", [ { name: "api1", args: { endpoint: "/users" } }, { name: "api2", args: { endpoint: "/posts" } }, ]); @@ -478,7 +915,7 @@ const results = ctx.MCP.CallToolsParallel("toolkit", [ Lists available prompts from an MCP client. ```javascript -const prompts = ctx.MCP.ListPrompts("prompt_library"); +const available_prompts = ctx.MCP.ListPrompts("prompt_library"); ``` #### `ctx.MCP.GetPrompt(client, name, args?)` @@ -486,7 +923,7 @@ const prompts = ctx.MCP.ListPrompts("prompt_library"); Retrieves a specific prompt. ```javascript -const prompt = ctx.MCP.GetPrompt("prompt_library", "code_review", { +const review_prompt = ctx.MCP.GetPrompt("prompt_library", "code_review", { language: "javascript", }); ``` @@ -515,7 +952,7 @@ Here's a comprehensive example using various Context API features: function Next(ctx, messages, completion, tools) { try { // Create trace node for custom processing - const processNode = ctx.Trace.Add( + const process_node = ctx.Trace.Add( { completion, tools }, { label: "Custom Processing", @@ -531,36 +968,39 @@ function Next(ctx, messages, completion, tools) { }); // Send progress message and capture message ID - const progressId = ctx.Send("Searching for articles..."); + const progress_id = ctx.Send("Searching for articles..."); // Call MCP tool for additional data - const searchResults = ctx.MCP.CallTool("search_engine", "search", { + const search_results = ctx.MCP.CallTool("search_engine", "search", { query: "latest AI news", limit: 5, }); // Update trace with results - processNode.SetMetadata("search_results_count", searchResults.length); + process_node.SetMetadata("search_results_count", search_results.length); // Update the progress message with results - ctx.Replace(progressId, `Found ${searchResults.length} relevant articles.`); + ctx.Replace( + progress_id, + `Found ${search_results.length} relevant articles.` + ); // Log the message ID for tracking - ctx.Trace.Debug("Updated progress message", { message_id: progressId }); + ctx.Trace.Debug("Updated progress message", { message_id: progress_id }); // Process and format response - const enhancedResponse = { + const enhanced_response = { text: completion.content, - sources: searchResults, + sources: search_results, timestamp: Date.now(), }; // Mark node as complete - processNode.Complete(enhancedResponse); + process_node.Complete(enhanced_response); // Return enhanced response return { - data: enhancedResponse, + data: enhanced_response, done: true, }; } catch (error) { diff --git a/agent/context/context.go b/agent/context/context.go index 3dbc1553..8ec38aa0 100644 --- a/agent/context/context.go +++ b/agent/context/context.go @@ -375,3 +375,37 @@ func (ctx *Context) TraceID() string { } return "" } + +// recordMessageMetadata records metadata for a sent message +// Used to inherit BlockID and ThreadID in subsequent delta operations +func (ctx *Context) recordMessageMetadata(msg *message.Message) { + if msg.MessageID == "" { + return + } + + ctx.metadataMu.Lock() + defer ctx.metadataMu.Unlock() + + if ctx.messageMetadata == nil { + ctx.messageMetadata = make(map[string]*MessageMetadata) + } + + ctx.messageMetadata[msg.MessageID] = &MessageMetadata{ + MessageID: msg.MessageID, + BlockID: msg.BlockID, + ThreadID: msg.ThreadID, + } +} + +// getMessageMetadata retrieves metadata for a message by ID +// Returns nil if message metadata is not found +func (ctx *Context) getMessageMetadata(messageID string) *MessageMetadata { + ctx.metadataMu.RLock() + defer ctx.metadataMu.RUnlock() + + if ctx.messageMetadata == nil { + return nil + } + + return ctx.messageMetadata[messageID] +} diff --git a/agent/context/jsapi.go b/agent/context/jsapi.go index 55741d47..7b3b003d 100644 --- a/agent/context/jsapi.go +++ b/agent/context/jsapi.go @@ -52,6 +52,14 @@ func (ctx *Context) NewObject(v8ctx *v8go.Context) (*v8go.Value, error) { // Set methods jsObject.Set("Send", ctx.sendMethod(v8ctx.Isolate())) jsObject.Set("Replace", ctx.replaceMethod(v8ctx.Isolate())) + jsObject.Set("Append", ctx.appendMethod(v8ctx.Isolate())) + jsObject.Set("Merge", ctx.mergeMethod(v8ctx.Isolate())) + jsObject.Set("Set", ctx.setMethod(v8ctx.Isolate())) + + // Set ID generator methods + jsObject.Set("MessageID", ctx.messageIDMethod(v8ctx.Isolate())) + jsObject.Set("BlockID", ctx.blockIDMethod(v8ctx.Isolate())) + jsObject.Set("ThreadID", ctx.threadIDMethod(v8ctx.Isolate())) // Set MCP object jsObject.Set("MCP", ctx.newMCPObject(v8ctx.Isolate())) @@ -198,10 +206,11 @@ func (ctx *Context) createTraceObject(v8ctx *v8go.Context) *v8go.Value { return traceObj } -// sendMethod implements ctx.Send(message) +// sendMethod implements ctx.Send(message, blockId?) // Usage: const messageId = ctx.Send({ type: "text", props: { content: "Hello" } }) // Usage: const messageId = ctx.Send("Hello") // shorthand for text message -// Automatically generates ID and flushes output +// Usage: const messageId = ctx.Send("Hello", "B1") // specify block ID +// Automatically generates MessageID and BlockID (if not specified), flushes output // Returns: message_id (string) func (ctx *Context) sendMethod(iso *v8go.Isolate) *v8go.FunctionTemplate { return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value { @@ -218,6 +227,12 @@ func (ctx *Context) sendMethod(iso *v8go.Isolate) *v8go.FunctionTemplate { return bridge.JsException(v8ctx, "invalid message: "+err.Error()) } + // Get optional blockId argument (second argument) + // Note: message object's block_id has higher priority + if len(args) >= 2 && args[1].IsString() && msg.BlockID == "" { + msg.BlockID = args[1].String() + } + // Generate unique MessageID if not provided if msg.MessageID == "" { if ctx.IDGenerator != nil { @@ -227,7 +242,7 @@ func (ctx *Context) sendMethod(iso *v8go.Isolate) *v8go.FunctionTemplate { } } - // Call ctx.Send + // Call ctx.Send (will auto-generate BlockID if still empty) if err := ctx.Send(msg); err != nil { return bridge.JsException(v8ctx, "Send failed: "+err.Error()) } @@ -300,6 +315,268 @@ func (ctx *Context) replaceMethod(iso *v8go.Isolate) *v8go.FunctionTemplate { }) } +// appendMethod implements ctx.Append(messageId, content, path?) +// Usage: ctx.Append(messageId, "more text") // append to default content path +// Usage: ctx.Append(messageId, "more text", "props.content") // append to specific path +// Usage: ctx.Append(messageId, { type: "text", props: { content: "more text" } }) +// Usage: ctx.Append(messageId, { props: { content: "more text" } }, "props.data") // append to custom path +// Appends content to an existing message (delta append operation) +// Automatically flushes output +// Returns: message_id (string) +func (ctx *Context) appendMethod(iso *v8go.Isolate) *v8go.FunctionTemplate { + return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value { + v8ctx := info.Context() + args := info.Args() + + // Validate arguments + if len(args) < 2 { + return bridge.JsException(v8ctx, "Append requires messageId and content arguments") + } + + // Get message ID (first argument) + if !args[0].IsString() { + return bridge.JsException(v8ctx, "messageId must be a string") + } + messageID := args[0].String() + + // Parse content argument (second argument) + msg, err := parseMessage(v8ctx, args[1]) + if err != nil { + return bridge.JsException(v8ctx, "invalid content: "+err.Error()) + } + + // Get optional path argument (third argument) + deltaPath := "" + if len(args) >= 3 && args[2].IsString() { + deltaPath = args[2].String() + } + + // Set message ID to the provided ID + msg.MessageID = messageID + + // Set delta mode for append + msg.Delta = true + msg.DeltaAction = message.DeltaAppend + msg.DeltaPath = deltaPath // Empty path means append to default content, or specify custom path + + // Call ctx.Send + if err := ctx.Send(msg); err != nil { + return bridge.JsException(v8ctx, "Append failed: "+err.Error()) + } + + // Automatically flush after sending + if err := ctx.Flush(); err != nil { + return bridge.JsException(v8ctx, "Flush failed: "+err.Error()) + } + + // Return the message ID + returnID, err := v8go.NewValue(iso, messageID) + if err != nil { + return bridge.JsException(v8ctx, "Failed to create return value: "+err.Error()) + } + return returnID + }) +} + +// mergeMethod implements ctx.Merge(messageId, data, path?) +// Usage: ctx.Merge(messageId, { key: "value" }) // merge to default object path +// Usage: ctx.Merge(messageId, { status: "done" }, "props") // merge to specific path +// Usage: ctx.Merge(messageId, { props: { status: "done", progress: 100 } }) +// Merges data into an existing message object (delta merge operation) +// Automatically flushes output +// Returns: message_id (string) +func (ctx *Context) mergeMethod(iso *v8go.Isolate) *v8go.FunctionTemplate { + return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value { + v8ctx := info.Context() + args := info.Args() + + // Validate arguments + if len(args) < 2 { + return bridge.JsException(v8ctx, "Merge requires messageId and data arguments") + } + + // Get message ID (first argument) + if !args[0].IsString() { + return bridge.JsException(v8ctx, "messageId must be a string") + } + messageID := args[0].String() + + // Parse data argument (second argument) + msg, err := parseMessage(v8ctx, args[1]) + if err != nil { + return bridge.JsException(v8ctx, "invalid data: "+err.Error()) + } + + // Get optional path argument (third argument) + deltaPath := "" + if len(args) >= 3 && args[2].IsString() { + deltaPath = args[2].String() + } + + // Set message ID to the provided ID + msg.MessageID = messageID + + // Set delta mode for merge + msg.Delta = true + msg.DeltaAction = message.DeltaMerge + msg.DeltaPath = deltaPath // Empty path means merge to default object, or specify custom path + + // Call ctx.Send + if err := ctx.Send(msg); err != nil { + return bridge.JsException(v8ctx, "Merge failed: "+err.Error()) + } + + // Automatically flush after sending + if err := ctx.Flush(); err != nil { + return bridge.JsException(v8ctx, "Flush failed: "+err.Error()) + } + + // Return the message ID + returnID, err := v8go.NewValue(iso, messageID) + if err != nil { + return bridge.JsException(v8ctx, "Failed to create return value: "+err.Error()) + } + return returnID + }) +} + +// setMethod implements ctx.Set(messageId, data, path) +// Usage: ctx.Set(messageId, "value", "props.newField") // set new field at specific path +// Usage: ctx.Set(messageId, { newKey: "value" }, "props") // set new fields in props +// Sets a new field or value in an existing message (delta set operation) +// Automatically flushes output +// Returns: message_id (string) +func (ctx *Context) setMethod(iso *v8go.Isolate) *v8go.FunctionTemplate { + return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value { + v8ctx := info.Context() + args := info.Args() + + // Validate arguments (path is required for Set operation) + if len(args) < 3 { + return bridge.JsException(v8ctx, "Set requires messageId, data, and path arguments") + } + + // Get message ID (first argument) + if !args[0].IsString() { + return bridge.JsException(v8ctx, "messageId must be a string") + } + messageID := args[0].String() + + // Parse data argument (second argument) + msg, err := parseMessage(v8ctx, args[1]) + if err != nil { + return bridge.JsException(v8ctx, "invalid data: "+err.Error()) + } + + // Get path argument (third argument - required) + if !args[2].IsString() { + return bridge.JsException(v8ctx, "path must be a string") + } + deltaPath := args[2].String() + + if deltaPath == "" { + return bridge.JsException(v8ctx, "path cannot be empty for Set operation") + } + + // Set message ID to the provided ID + msg.MessageID = messageID + + // Set delta mode for set + msg.Delta = true + msg.DeltaAction = message.DeltaSet + msg.DeltaPath = deltaPath // Path is required for Set operation + + // Call ctx.Send + if err := ctx.Send(msg); err != nil { + return bridge.JsException(v8ctx, "Set failed: "+err.Error()) + } + + // Automatically flush after sending + if err := ctx.Flush(); err != nil { + return bridge.JsException(v8ctx, "Flush failed: "+err.Error()) + } + + // Return the message ID + returnID, err := v8go.NewValue(iso, messageID) + if err != nil { + return bridge.JsException(v8ctx, "Failed to create return value: "+err.Error()) + } + return returnID + }) +} + +// messageIDMethod implements ctx.MessageID() +// Usage: const msgId = ctx.MessageID() // Returns: "M1", "M2", "M3"... +// Generates a unique message ID for manual message management +// Returns: message_id (string) +func (ctx *Context) messageIDMethod(iso *v8go.Isolate) *v8go.FunctionTemplate { + return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value { + v8ctx := info.Context() + + var messageID string + if ctx.IDGenerator != nil { + messageID = ctx.IDGenerator.GenerateMessageID() + } else { + messageID = output.GenerateID() + } + + // Return the generated ID + id, err := v8go.NewValue(iso, messageID) + if err != nil { + return bridge.JsException(v8ctx, "Failed to generate message ID: "+err.Error()) + } + return id + }) +} + +// blockIDMethod implements ctx.BlockID() +// Usage: const blockId = ctx.BlockID() // Returns: "B1", "B2", "B3"... +// Generates a unique block ID for grouping messages +// Returns: block_id (string) +func (ctx *Context) blockIDMethod(iso *v8go.Isolate) *v8go.FunctionTemplate { + return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value { + v8ctx := info.Context() + + var blockID string + if ctx.IDGenerator != nil { + blockID = ctx.IDGenerator.GenerateBlockID() + } else { + blockID = output.GenerateID() + } + + // Return the generated ID + id, err := v8go.NewValue(iso, blockID) + if err != nil { + return bridge.JsException(v8ctx, "Failed to generate block ID: "+err.Error()) + } + return id + }) +} + +// threadIDMethod implements ctx.ThreadID() +// Usage: const threadId = ctx.ThreadID() // Returns: "T1", "T2", "T3"... +// Generates a unique thread ID for concurrent operations +// Returns: thread_id (string) +func (ctx *Context) threadIDMethod(iso *v8go.Isolate) *v8go.FunctionTemplate { + return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value { + v8ctx := info.Context() + + var threadID string + if ctx.IDGenerator != nil { + threadID = ctx.IDGenerator.GenerateThreadID() + } else { + threadID = output.GenerateID() + } + + // Return the generated ID + id, err := v8go.NewValue(iso, threadID) + if err != nil { + return bridge.JsException(v8ctx, "Failed to generate thread ID: "+err.Error()) + } + return id + }) +} + // sendGroupMethod implements ctx.SendGroup(group) // Usage: ctx.SendGroup({ id: "group1", messages: [...] }) // Automatically generates IDs, sends group_start/group_end events, and flushes output diff --git a/agent/context/jsapi_output_test.go b/agent/context/jsapi_output_test.go index d502de94..78af5e72 100644 --- a/agent/context/jsapi_output_test.go +++ b/agent/context/jsapi_output_test.go @@ -372,3 +372,404 @@ func TestJsValueSendChainedCalls(t *testing.T) { } assert.Equal(t, true, result["success"], "Chained Send calls should succeed") } + +// TestJsValueIDGenerators test ID generator methods +func TestJsValueIDGenerators(t *testing.T) { + + test.Prepare(t, config.Conf) + defer test.Clean() + + cxt := &Context{ + ChatID: "test-chat-id", + AssistantID: "test-assistant-id", + Context: context.Background(), + Accept: "standard", + Locale: "en", + Writer: newMockResponseWriter(), + IDGenerator: message.NewIDGenerator(), + } + + res, err := v8.Call(v8.CallOptions{}, ` + function test(ctx) { + try { + // Test MessageID generator + const msgId1 = ctx.MessageID(); + const msgId2 = ctx.MessageID(); + + // Test BlockID generator + const blockId1 = ctx.BlockID(); + const blockId2 = ctx.BlockID(); + + // Test ThreadID generator + const threadId1 = ctx.ThreadID(); + const threadId2 = ctx.ThreadID(); + + // Verify IDs are strings and sequential + if (typeof msgId1 !== 'string' || typeof msgId2 !== 'string') { + throw new Error('MessageID should return string'); + } + if (typeof blockId1 !== 'string' || typeof blockId2 !== 'string') { + throw new Error('BlockID should return string'); + } + if (typeof threadId1 !== 'string' || typeof threadId2 !== 'string') { + throw new Error('ThreadID should return string'); + } + + // Verify they follow the pattern (M1, M2, B1, B2, T1, T2) + if (!msgId1.startsWith('M') || !msgId2.startsWith('M')) { + throw new Error('MessageID should start with M'); + } + if (!blockId1.startsWith('B') || !blockId2.startsWith('B')) { + throw new Error('BlockID should start with B'); + } + if (!threadId1.startsWith('T') || !threadId2.startsWith('T')) { + throw new Error('ThreadID should start with T'); + } + + return { + success: true, + msgId1: msgId1, + msgId2: msgId2, + blockId1: blockId1, + blockId2: blockId2, + threadId1: threadId1, + threadId2: threadId2 + }; + } catch (error) { + return { success: false, error: error.message }; + } + }`, cxt) + if err != nil { + t.Fatalf("Call failed: %v", err) + } + + result, ok := res.(map[string]interface{}) + if !ok { + t.Fatalf("Expected map result, got %T", res) + } + assert.Equal(t, true, result["success"], "ID generators should succeed") +} + +// TestJsValueSendWithBlockID test Send with block_id parameter +func TestJsValueSendWithBlockID(t *testing.T) { + + test.Prepare(t, config.Conf) + defer test.Clean() + + cxt := &Context{ + ChatID: "test-chat-id", + AssistantID: "test-assistant-id", + Context: context.Background(), + Accept: "standard", + Locale: "en", + Writer: newMockResponseWriter(), + IDGenerator: message.NewIDGenerator(), + } + + res, err := v8.Call(v8.CallOptions{}, ` + function test(ctx) { + try { + // Generate block ID manually + const blockId = ctx.BlockID(); + + // Send multiple messages with same block ID + const msg1 = ctx.Send("Message 1", blockId); + const msg2 = ctx.Send("Message 2", blockId); + const msg3 = ctx.Send("Message 3", blockId); + + // Send message with block_id in object (higher priority) + const msg4 = ctx.Send({ + type: "text", + props: { content: "Message 4" }, + block_id: "B_custom" + }, blockId); // blockId parameter should be ignored + + return { + success: true, + msg1: msg1, + msg2: msg2, + msg3: msg3, + msg4: msg4, + blockId: blockId + }; + } catch (error) { + return { success: false, error: error.message }; + } + }`, cxt) + if err != nil { + t.Fatalf("Call failed: %v", err) + } + + result, ok := res.(map[string]interface{}) + if !ok { + t.Fatalf("Expected map result, got %T", res) + } + assert.Equal(t, true, result["success"], "Send with blockId should succeed") +} + +// TestJsValueReplace test ctx.Replace method +func TestJsValueReplace(t *testing.T) { + + test.Prepare(t, config.Conf) + defer test.Clean() + + cxt := &Context{ + ChatID: "test-chat-id", + AssistantID: "test-assistant-id", + Context: context.Background(), + Accept: "standard", + Locale: "en", + Writer: newMockResponseWriter(), + IDGenerator: message.NewIDGenerator(), + } + + res, err := v8.Call(v8.CallOptions{}, ` + function test(ctx) { + try { + // Send initial message + const msgId = ctx.Send("Initial content"); + + // Replace with new content + ctx.Replace(msgId, "Updated content"); + + // Replace with object + ctx.Replace(msgId, { + type: "text", + props: { content: "Final content" } + }); + + return { success: true, msgId: msgId }; + } catch (error) { + return { success: false, error: error.message }; + } + }`, cxt) + if err != nil { + t.Fatalf("Call failed: %v", err) + } + + result, ok := res.(map[string]interface{}) + if !ok { + t.Fatalf("Expected map result, got %T", res) + } + assert.Equal(t, true, result["success"], "Replace should succeed") +} + +// TestJsValueAppend test ctx.Append method +func TestJsValueAppend(t *testing.T) { + + test.Prepare(t, config.Conf) + defer test.Clean() + + cxt := &Context{ + ChatID: "test-chat-id", + AssistantID: "test-assistant-id", + Context: context.Background(), + Accept: "standard", + Locale: "en", + Writer: newMockResponseWriter(), + IDGenerator: message.NewIDGenerator(), + } + + res, err := v8.Call(v8.CallOptions{}, ` + function test(ctx) { + try { + // Send initial message + const msgId = ctx.Send("Hello"); + + // Append to default path + ctx.Append(msgId, " World"); + ctx.Append(msgId, "!"); + + // Append to specific path + const msgId2 = ctx.Send({ + type: "data", + props: { content: "Line 1\n" } + }); + ctx.Append(msgId2, "Line 2\n", "props.content"); + ctx.Append(msgId2, "Line 3\n", "props.content"); + + return { success: true, msgId: msgId, msgId2: msgId2 }; + } catch (error) { + return { success: false, error: error.message }; + } + }`, cxt) + if err != nil { + t.Fatalf("Call failed: %v", err) + } + + result, ok := res.(map[string]interface{}) + if !ok { + t.Fatalf("Expected map result, got %T", res) + } + assert.Equal(t, true, result["success"], "Append should succeed") +} + +// TestJsValueMerge test ctx.Merge method +func TestJsValueMerge(t *testing.T) { + + test.Prepare(t, config.Conf) + defer test.Clean() + + cxt := &Context{ + ChatID: "test-chat-id", + AssistantID: "test-assistant-id", + Context: context.Background(), + Accept: "standard", + Locale: "en", + Writer: newMockResponseWriter(), + IDGenerator: message.NewIDGenerator(), + } + + res, err := v8.Call(v8.CallOptions{}, ` + function test(ctx) { + try { + // Send initial message with object + const msgId = ctx.Send({ + type: "status", + props: { + status: "running", + progress: 0, + started: true + } + }); + + // Merge updates (keeps other fields) + ctx.Merge(msgId, { + type: "status", + props: { progress: 50 } + }, "props"); + ctx.Merge(msgId, { + type: "status", + props: { progress: 100, status: "completed" } + }, "props"); + + return { success: true, msgId: msgId }; + } catch (error) { + return { success: false, error: error.message }; + } + }`, cxt) + if err != nil { + t.Fatalf("Call failed: %v", err) + } + + result, ok := res.(map[string]interface{}) + if !ok { + t.Fatalf("Expected map result, got %T", res) + } + if !result["success"].(bool) { + t.Logf("Error: %v", result["error"]) + } + assert.Equal(t, true, result["success"], "Merge should succeed") +} + +// TestJsValueSet test ctx.Set method +func TestJsValueSet(t *testing.T) { + + test.Prepare(t, config.Conf) + defer test.Clean() + + cxt := &Context{ + ChatID: "test-chat-id", + AssistantID: "test-assistant-id", + Context: context.Background(), + Accept: "standard", + Locale: "en", + Writer: newMockResponseWriter(), + IDGenerator: message.NewIDGenerator(), + } + + res, err := v8.Call(v8.CallOptions{}, ` + function test(ctx) { + try { + // Send initial message + const msgId = ctx.Send({ + type: "result", + props: { content: "Initial" } + }); + + // Set new fields + ctx.Set(msgId, { + type: "result", + props: { status: "success" } + }, "props.status"); + ctx.Set(msgId, { + type: "result", + props: { timestamp: Date.now() } + }, "props.timestamp"); + ctx.Set(msgId, { + type: "result", + props: { metadata: { duration: 1500 } } + }, "props.metadata"); + + return { success: true, msgId: msgId }; + } catch (error) { + return { success: false, error: error.message }; + } + }`, cxt) + if err != nil { + t.Fatalf("Call failed: %v", err) + } + + result, ok := res.(map[string]interface{}) + if !ok { + t.Fatalf("Expected map result, got %T", res) + } + if !result["success"].(bool) { + t.Logf("Error: %v", result["error"]) + } + assert.Equal(t, true, result["success"], "Set should succeed") +} + +// TestJsValueBlockIDInheritance test that delta operations inherit block_id +func TestJsValueBlockIDInheritance(t *testing.T) { + + test.Prepare(t, config.Conf) + defer test.Clean() + + cxt := &Context{ + ChatID: "test-chat-id", + AssistantID: "test-assistant-id", + Context: context.Background(), + Accept: "standard", + Locale: "en", + Writer: newMockResponseWriter(), + IDGenerator: message.NewIDGenerator(), + } + + res, err := v8.Call(v8.CallOptions{}, ` + function test(ctx) { + try { + // Send message with block_id + const blockId = ctx.BlockID(); + const msgId = ctx.Send("Initial message", blockId); + + // Delta operations should inherit block_id automatically + ctx.Append(msgId, " appended"); + ctx.Replace(msgId, "Replaced message"); + ctx.Merge(msgId, { + type: "text", + props: { status: "done" } + }, "props"); + ctx.Set(msgId, { + type: "text", + props: { state: "final" } + }, "props.state"); + + return { success: true, msgId: msgId, blockId: blockId }; + } catch (error) { + return { success: false, error: error.message }; + } + }`, cxt) + if err != nil { + t.Fatalf("Call failed: %v", err) + } + + result, ok := res.(map[string]interface{}) + if !ok { + t.Fatalf("Expected map result, got %T", res) + } + if !result["success"].(bool) { + t.Logf("Error: %v", result["error"]) + } + assert.Equal(t, true, result["success"], "Delta operations should inherit block_id") +} diff --git a/agent/context/output.go b/agent/context/output.go index 689744ba..3c146baf 100644 --- a/agent/context/output.go +++ b/agent/context/output.go @@ -6,15 +6,51 @@ import ( ) // Send sends a message via the output module +// Automatically manages BlockID, ThreadID, and metadata for delta operations +// - For delta operations: inherits BlockID and ThreadID from original message +// - For new messages: auto-generates BlockID if not specified, sets ThreadID from Stack +// - Records metadata for all sent messages to enable delta inheritance func (ctx *Context) Send(msg *message.Message) error { output, err := ctx.getOutput() if err != nil { return err } + + // === Delta operations: Auto-inherit BlockID and ThreadID === + if msg.Delta && msg.MessageID != "" { + if metadata := ctx.getMessageMetadata(msg.MessageID); metadata != nil { + // Inherit BlockID if not specified + if msg.BlockID == "" { + msg.BlockID = metadata.BlockID + } + // Inherit ThreadID if not specified + if msg.ThreadID == "" { + msg.ThreadID = metadata.ThreadID + } + } + } + + // === Non-delta operations: Auto-set fields === + if !msg.Delta { + // Auto-set ThreadID for non-root Stack (nested agent calls) + if msg.ThreadID == "" && ctx.Stack != nil && !ctx.Stack.IsRoot() { + msg.ThreadID = ctx.Stack.ID + } + + // BlockID is NOT auto-generated by default (only manually specified in special cases) + // Example: Send a web card after LLM output, group them in the same Block + // Developers can specify via ctx.Send(message, blockId) or message.block_id + } + + // === Record metadata for subsequent delta operations === + ctx.recordMessageMetadata(msg) + + // === Actually send the message === return output.Send(msg) } // SendGroup sends a group of messages via the output module +// Deprecated: This method is deprecated and will be removed in future versions func (ctx *Context) SendGroup(group *message.Group) error { output, err := ctx.getOutput() if err != nil { diff --git a/agent/context/types.go b/agent/context/types.go index fd982a3e..8e5b39a0 100644 --- a/agent/context/types.go +++ b/agent/context/types.go @@ -194,21 +194,31 @@ type Skip struct { Trace bool `json:"trace"` // Skip trace logging } +// MessageMetadata stores metadata for sent messages +// Used to inherit BlockID and ThreadID in delta operations +type MessageMetadata struct { + MessageID string // Message ID + BlockID string // Block ID + ThreadID string // Thread ID +} + // Context the context type Context struct { // Context context.Context - ID string `json:"id"` // Context ID for external interrupt identification - Space plan.Space `json:"-"` // Shared data space, it will be used to share data between the request and the call - Cache store.Store `json:"-"` // Cache store, it will be used to store the message cache, default is "__yao.agent.cache" - Stack *Stack `json:"-"` // Stack, current active stack of the request - Stacks map[string]*Stack `json:"-"` // Stacks, all stacks in this request (for trace logging) - Writer Writer `json:"-"` // Writer, it will be used to write response data to the client - Skip *Skip `json:"skip,omitempty"` // Skip configuration (history, trace, etc.), nil means don't skip anything - trace traceTypes.Manager `json:"-"` // Trace manager, lazy initialized on first access - output *output.Output `json:"-"` // Output, it will be used to write response data to the client - IDGenerator *message.IDGenerator `json:"-"` // ID generator for this context (chunk, message, block, thread IDs) + ID string `json:"id"` // Context ID for external interrupt identification + Space plan.Space `json:"-"` // Shared data space, it will be used to share data between the request and the call + Cache store.Store `json:"-"` // Cache store, it will be used to store the message cache, default is "__yao.agent.cache" + Stack *Stack `json:"-"` // Stack, current active stack of the request + Stacks map[string]*Stack `json:"-"` // Stacks, all stacks in this request (for trace logging) + Writer Writer `json:"-"` // Writer, it will be used to write response data to the client + Skip *Skip `json:"skip,omitempty"` // Skip configuration (history, trace, etc.), nil means don't skip anything + trace traceTypes.Manager `json:"-"` // Trace manager, lazy initialized on first access + output *output.Output `json:"-"` // Output, it will be used to write response data to the client + IDGenerator *message.IDGenerator `json:"-"` // ID generator for this context (chunk, message, block, thread IDs) + messageMetadata map[string]*MessageMetadata `json:"-"` // Message metadata cache for delta operations (inheriting BlockID/ThreadID) + metadataMu sync.RWMutex `json:"-"` // Mutex for concurrent access to messageMetadata // Model capabilities (set by assistant, used by output adapters) Capabilities *ModelCapabilities `json:"-"` // Model capabilities for the current connector