Add message metadata handling and new context methods for message operations
- Introduced recordMessageMetadata and getMessageMetadata methods to manage metadata for sent messages, enabling BlockID and ThreadID inheritance in delta operations. - Implemented new methods (Append, Merge, Set) in the context to enhance message management capabilities, allowing for more flexible message updates. - Updated the Send method to automatically manage BlockID and ThreadID for delta operations, improving message handling consistency. - Enhanced JSAPI documentation to reflect new methods and usage patterns, improving developer experience and clarity.
This commit is contained in:
parent
c11bc0a1ae
commit
ab240443f8
6 changed files with 1264 additions and 66 deletions
|
|
@ -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<string, any>; // 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) {
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue