feat(tasks): add inherited context and resilient LLM execution

Add read-only task context inheritance, ordered task-level LLM failover, live Worker controls, cascading task cleanup, FIFO task concurrency, cancellation diagnostics, UI updates, tests, build script, and changelog.
This commit is contained in:
pphua committed 2026-08-20 18:34:24 +08:00
1 parent 7090b034f8
commit 9793b522d9
82 files changed
+11094 -1712

No files matched your search

+34 -7
View File
@@ -16,6 +16,27 @@ import (
"github.com/Autumn-27/artex/db"
)
type taskIDContextKey struct{}
// WithTaskID attaches the owning task registry id to an LLM call. Session ids
// are based on exploration ids, which are not interchangeable with task ids.
func WithTaskID(ctx context.Context, taskID string) context.Context {
taskID = strings.TrimSpace(taskID)
if taskID == "" {
return ctx
}
return context.WithValue(ctx, taskIDContextKey{}, taskID)
}
// TaskIDFrom returns the explicit task registry id attached by the task runtime.
func TaskIDFrom(ctx context.Context) string {
if ctx == nil {
return ""
}
taskID, _ := ctx.Value(taskIDContextKey{}).(string)
return strings.TrimSpace(taskID)
}
// Recorder wraps an llm.Provider and records every completion call.
type Recorder struct {
inner llm.Provider
@@ -70,7 +91,13 @@ func (r *Recorder) Stream(ctx context.Context, req llm.CompletionRequest) iter.S
}
start := time.Now()
session := transcript.SessionIDFrom(ctx)
taskID, worker := parseSession(session)
parsedID, worker := parseSession(session)
taskID := TaskIDFrom(ctx)
if taskID == "" {
// Backward compatibility for non-task callers. For task calls, the task
// runtime always supplies the registry id explicitly.
taskID = parsedID
}
// Serialize the request for storage.
reqBody := serializeRequest(req)
@@ -120,7 +147,9 @@ func (r *Recorder) Stream(ctx context.Context, req llm.CompletionRequest) iter.S
}
}
// record persists one LLM call to PostgreSQL (async, best-effort).
// record persists one LLM call to PostgreSQL before the provider stream returns.
// Keeping the write inside the owning task operation means task deletion can
// drain calls and then remove records without a late async insert recreating one.
func (r *Recorder) record(req llm.CompletionRequest, session, taskID, worker, reqBody string, start time.Time, text, thinking string, usage llm.Usage, stopReason string, streamErr error) {
latency := int(time.Since(start).Milliseconds())
status := "ok"
@@ -169,11 +198,9 @@ func (r *Recorder) record(req llm.CompletionRequest, session, taskID, worker, re
ResponseBody: string(respBody),
}
go func() {
if err := r.pg.InsertLLMRecord(rec); err != nil {
log.Printf("[llmrec] insert: %v", err)
}
}()
if err := r.pg.InsertLLMRecord(rec); err != nil {
log.Printf("[llmrec] insert: %v", err)
}
}
// serializeRequest builds a JSON representation of the completion request.
+29
View File
@@ -0,0 +1,29 @@
package llmrec
import (
"context"
"testing"
)
func TestTaskIDContextUsesExplicitRegistryID(t *testing.T) {
ctx := WithTaskID(context.Background(), " 42 ")
if got := TaskIDFrom(ctx); got != "42" {
t.Fatalf("TaskIDFrom()=%q want 42", got)
}
if got := TaskIDFrom(WithTaskID(ctx, " ")); got != "42" {
t.Fatalf("blank task id should preserve parent context, got %q", got)
}
if got := TaskIDFrom(nil); got != "" {
t.Fatalf("nil context returned %q", got)
}
}
func TestParseSessionFallbackIsExplorationScoped(t *testing.T) {
taskID, worker := parseSession("exp12-worker-i99")
if taskID != "12" || worker != "worker" {
t.Fatalf("parseSession()=(%q,%q)", taskID, worker)
}
if taskID, worker := parseSession("not-a-task"); taskID != "" || worker != "" {
t.Fatalf("unexpected non-session parse: (%q,%q)", taskID, worker)
}
}