Changeset 0.29.2 (#197)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit is contained in:
@@ -25,9 +25,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.starlark.net/starlark"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/events"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/providers"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/sandbox"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/tools"
|
||||
)
|
||||
|
||||
@@ -79,6 +82,9 @@ type LoopConfig struct {
|
||||
ConfigID string
|
||||
Budget LoopBudget
|
||||
Streaming bool // true = StreamCompletion, false = ChatCompletion
|
||||
// v0.29.2: extension tool dispatch (nil = no extension tools)
|
||||
Runner *sandbox.Runner
|
||||
ExtTools map[string]*store.PackageRegistration // toolName → package
|
||||
}
|
||||
|
||||
// ── Sink Interface ─────────────────────────────
|
||||
@@ -398,6 +404,12 @@ func executeToolCall(ctx context.Context, lcfg LoopConfig, call tools.ToolCall)
|
||||
return result
|
||||
}
|
||||
|
||||
// v0.29.2: Starlark extension tool
|
||||
if pkg, ok := lcfg.ExtTools[call.Name]; ok && lcfg.Runner != nil {
|
||||
log.Printf("🔧 Executing tool (extension): %s (call %s, pkg %s)", call.Name, call.ID, pkg.ID)
|
||||
return executeExtensionTool(ctx, lcfg, pkg, call)
|
||||
}
|
||||
|
||||
// Browser bridge (only available when hub is connected)
|
||||
if lcfg.Hub != nil && lcfg.Hub.IsConnected(lcfg.ExecCtx.UserID) {
|
||||
log.Printf("🔧 Executing tool (browser): %s (call %s)", call.Name, call.ID)
|
||||
@@ -414,6 +426,132 @@ func executeToolCall(ctx context.Context, lcfg LoopConfig, call tools.ToolCall)
|
||||
}
|
||||
}
|
||||
|
||||
// executeExtensionTool calls a starlark extension's on_tool_call entry point
|
||||
// and serializes the return value to a JSON string for the tool result.
|
||||
func executeExtensionTool(ctx context.Context, lcfg LoopConfig, pkg *store.PackageRegistration, call tools.ToolCall) tools.ToolResult {
|
||||
// Parse JSON arguments into a Starlark dict.
|
||||
var rawArgs map[string]interface{}
|
||||
if call.Arguments != "" {
|
||||
if err := json.Unmarshal([]byte(call.Arguments), &rawArgs); err != nil {
|
||||
return tools.ToolResult{
|
||||
ToolCallID: call.ID,
|
||||
Name: call.Name,
|
||||
Content: `{"error":"invalid tool arguments JSON"}`,
|
||||
IsError: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the call dict passed to on_tool_call(call).
|
||||
callDict := starlark.NewDict(3)
|
||||
_ = callDict.SetKey(starlark.String("tool_name"), starlark.String(call.Name))
|
||||
_ = callDict.SetKey(starlark.String("tool_call_id"), starlark.String(call.ID))
|
||||
_ = callDict.SetKey(starlark.String("arguments"), jsonToStarlark(rawArgs))
|
||||
|
||||
rc := &sandbox.RunContext{
|
||||
UserID: lcfg.ExecCtx.UserID,
|
||||
ChannelID: lcfg.ExecCtx.ChannelID,
|
||||
}
|
||||
val, output, err := lcfg.Runner.CallEntryPoint(ctx, pkg, "on_tool_call",
|
||||
starlark.Tuple{callDict}, nil, rc)
|
||||
if output != "" {
|
||||
log.Printf(" 🔧 ext tool %s print: %s", pkg.ID, output)
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("⚠️ ext tool %s on_tool_call error: %v", pkg.ID, err)
|
||||
return tools.ToolResult{
|
||||
ToolCallID: call.ID,
|
||||
Name: call.Name,
|
||||
Content: fmt.Sprintf(`{"error":%q}`, err.Error()),
|
||||
IsError: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize the Starlark return value to JSON.
|
||||
content, jsonErr := json.Marshal(starlarkValueToGo(val))
|
||||
if jsonErr != nil {
|
||||
content = []byte(fmt.Sprintf(`{"error":%q}`, jsonErr.Error()))
|
||||
}
|
||||
return tools.ToolResult{
|
||||
ToolCallID: call.ID,
|
||||
Name: call.Name,
|
||||
Content: string(content),
|
||||
}
|
||||
}
|
||||
|
||||
// jsonToStarlark recursively converts a decoded-JSON value (map/slice/scalar)
|
||||
// to its Starlark equivalent.
|
||||
func jsonToStarlark(v interface{}) starlark.Value {
|
||||
if v == nil {
|
||||
return starlark.None
|
||||
}
|
||||
switch val := v.(type) {
|
||||
case map[string]interface{}:
|
||||
d := starlark.NewDict(len(val))
|
||||
for k, v := range val {
|
||||
_ = d.SetKey(starlark.String(k), jsonToStarlark(v))
|
||||
}
|
||||
return d
|
||||
case []interface{}:
|
||||
elems := make([]starlark.Value, len(val))
|
||||
for i, v := range val {
|
||||
elems[i] = jsonToStarlark(v)
|
||||
}
|
||||
return starlark.NewList(elems)
|
||||
case string:
|
||||
return starlark.String(val)
|
||||
case float64:
|
||||
if val == float64(int64(val)) {
|
||||
return starlark.MakeInt64(int64(val))
|
||||
}
|
||||
return starlark.Float(val)
|
||||
case bool:
|
||||
return starlark.Bool(val)
|
||||
default:
|
||||
return starlark.String(fmt.Sprintf("%v", val))
|
||||
}
|
||||
}
|
||||
|
||||
// starlarkValueToGo recursively converts a Starlark value to a Go value
|
||||
// suitable for json.Marshal.
|
||||
func starlarkValueToGo(v starlark.Value) interface{} {
|
||||
if v == nil || v == starlark.None {
|
||||
return nil
|
||||
}
|
||||
switch val := v.(type) {
|
||||
case starlark.String:
|
||||
return string(val)
|
||||
case starlark.Int:
|
||||
i64, ok := val.Int64()
|
||||
if ok {
|
||||
return i64
|
||||
}
|
||||
return val.String()
|
||||
case starlark.Float:
|
||||
return float64(val)
|
||||
case starlark.Bool:
|
||||
return bool(val)
|
||||
case *starlark.Dict:
|
||||
m := make(map[string]interface{}, val.Len())
|
||||
for _, item := range val.Items() {
|
||||
k, ok := starlark.AsString(item[0])
|
||||
if !ok {
|
||||
k = item[0].String()
|
||||
}
|
||||
m[k] = starlarkValueToGo(item[1])
|
||||
}
|
||||
return m
|
||||
case *starlark.List:
|
||||
list := make([]interface{}, val.Len())
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
list[i] = starlarkValueToGo(val.Index(i))
|
||||
}
|
||||
return list
|
||||
default:
|
||||
return v.String()
|
||||
}
|
||||
}
|
||||
|
||||
// emitWorkspaceEvent sends workspace.file.changed when a write tool succeeds.
|
||||
func emitWorkspaceEvent(lcfg LoopConfig, call tools.ToolCall, result tools.ToolResult) {
|
||||
if lcfg.Hub == nil || result.IsError || lcfg.ExecCtx.WorkspaceID == "" {
|
||||
|
||||
Reference in New Issue
Block a user