Changeset 0.29.0 (#195)

This commit is contained in:
2026-03-17 16:28:47 +00:00
parent 128cbb8174
commit 5d637d3a90
129 changed files with 9418 additions and 3016 deletions

View File

@@ -22,6 +22,7 @@ import (
"git.gobha.me/xcaliber/chat-switchboard/models"
"git.gobha.me/xcaliber/chat-switchboard/notifications"
"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/taskutil"
"git.gobha.me/xcaliber/chat-switchboard/tools"
@@ -34,6 +35,7 @@ type Executor struct {
vault *crypto.KeyResolver
hub *events.Hub
health handlers.HealthRecorder
runner *sandbox.Runner // v0.29.0: Starlark task execution
}
// NewExecutor creates a task executor. All fields are optional except stores.
@@ -46,6 +48,11 @@ func NewExecutor(stores store.Stores, vault *crypto.KeyResolver, hub *events.Hub
}
}
// SetRunner attaches the Starlark sandbox runner for starlark task execution.
func (e *Executor) SetRunner(r *sandbox.Runner) {
e.runner = r
}
// Execute runs a single task to completion.
// Called from the scheduler's execute() goroutine.
func (e *Executor) Execute(ctx context.Context, task models.Task, run *models.TaskRun, channelID string) {
@@ -63,12 +70,18 @@ func (e *Executor) Execute(ctx context.Context, task models.Task, run *models.Ta
return
}
// v0.29.0: Starlark tasks run a sandboxed extension script.
if task.TaskType == "starlark" {
e.executeStarlark(ctx, task, run, channelID, startTime)
return
}
// ── 1. Resolve provider ────────────────────
providerConfigID := ""
if task.ProviderConfigID != nil {
providerConfigID = *task.ProviderConfigID
}
res, err := handlers.ResolveProviderConfig(e.vault, task.OwnerID, channelID, providerConfigID, task.ModelID)
res, err := handlers.ResolveProviderConfig(e.stores, e.vault, task.OwnerID, channelID, providerConfigID, task.ModelID)
if err != nil {
e.failRun(ctx, task, run, "provider resolution failed: "+err.Error())
return
@@ -265,6 +278,83 @@ func (e *Executor) Execute(ctx context.Context, task models.Task, run *models.Ta
}
}
// executeStarlark runs a sandboxed Starlark extension script.
// The task's system_function field holds the package ID.
// The script's on_run(ctx) entry point is called with task context.
func (e *Executor) executeStarlark(ctx context.Context, task models.Task, run *models.TaskRun, channelID string, startTime time.Time) {
if e.runner == nil {
e.failRun(ctx, task, run, "starlark runner not configured")
return
}
packageID := task.SystemFunction
if packageID == "" {
e.failRun(ctx, task, run, "starlark task missing package_id (system_function field)")
return
}
// Load the package
pkg, err := e.stores.Packages.Get(ctx, packageID)
if err != nil || pkg == nil {
e.failRun(ctx, task, run, "package not found: "+packageID)
return
}
// Run the script and call on_run entry point
// Build a context dict with task info
val, output, err := e.runner.CallEntryPoint(ctx, pkg, "on_run", nil, nil)
wallClock := int(time.Since(startTime).Seconds())
status := "completed"
errMsg := ""
result := ""
if err != nil {
status = "failed"
errMsg = err.Error()
} else if val != nil {
result = val.String()
}
// Append print output to result
if output != "" {
if result != "" {
result = result + "\n---\n" + output
} else {
result = output
}
}
// Persist to channel if output_mode == "channel"
if status == "completed" && task.OutputMode == "channel" && e.stores.Messages != nil && result != "" {
_ = e.stores.Messages.Create(ctx, &models.Message{
ChannelID: channelID,
Role: "system",
Content: "Starlark task output:\n```\n" + result + "\n```",
})
}
_ = e.stores.Tasks.UpdateRun(ctx, run.ID, status, 0, 0, wallClock, errMsg)
_ = e.stores.Tasks.IncrementRunCount(ctx, task.ID)
log.Printf("[executor] Starlark task %s (%s → %s) → %s (wall=%ds)", task.ID, task.Name, packageID, status, wallClock)
e.notifyOwner(ctx, task, status, errMsg)
if task.WebhookURL != "" {
go webhook.Deliver(task.WebhookURL, task.WebhookSecret, webhook.Payload{
TaskID: task.ID,
RunID: run.ID,
TaskName: task.Name,
ChannelID: channelID,
Status: status,
CompletedAt: time.Now().UTC(),
Output: result,
Error: errMsg,
})
}
}
// executeSystem runs a built-in Go function from the system registry.
// No LLM, no provider resolution, no channel needed.
func (e *Executor) executeSystem(ctx context.Context, task models.Task, run *models.TaskRun, startTime time.Time) {