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:
1 parent
7090b034f8
commit
9793b522d9
82 files changed
+11094
-1712
No files matched your search
+34
-7
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user