Ctrl K
ComunidadGuías…

internal/app/learning_runtime.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package app

import (
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"log/slog"
	"strings"
	"time"

	"github.com/yeomyeonggeori/blueclaw/internal/agentruntime"
	"github.com/yeomyeonggeori/blueclaw/internal/config"
	"github.com/yeomyeonggeori/blueclaw/internal/learning"
	"github.com/yeomyeonggeori/blueclaw/internal/task"
	"github.com/yeomyeonggeori/blueprotocol/agentcontract"
	"github.com/yeomyeonggeori/blueprotocol/model"
)

func newLearningCoordinator(runtimeConfiguration config.RuntimeConfiguration, store *learning.Store, languageModel model.LanguageModelProvider, logger *slog.Logger) (*learning.Coordinator, error) {
	if store == nil || strings.TrimSpace(runtimeConfiguration.Terminal.WorkspaceRootPath) == "" {
		return nil, nil
	}
	coordinator, errorValue := learning.NewCoordinator(
		runtimeConfiguration.Terminal.WorkspaceRootPath,
		store,
		learning.Reviewer{Model: languageModel},
		nil,
	)
	if coordinator != nil && logger != nil {
		coordinator.Report = func(reviewError error) {
			logger.Error("learning.review_failed", "error", reviewError.Error())
		}
	}
	return coordinator, errorValue
}

func learningTaskObserver(coordinator *learning.Coordinator, taskRunService *task.TaskRunService) func(agentruntime.TaskLaunchRequest) func(agentruntime.TaskLaunchResult, error) {
	if coordinator == nil || taskRunService == nil {
		return func(agentruntime.TaskLaunchRequest) func(agentruntime.TaskLaunchResult, error) {
			return func(agentruntime.TaskLaunchResult, error) {}
		}
	}
	return func(request agentruntime.TaskLaunchRequest) func(agentruntime.TaskLaunchResult, error) {
		if !coordinator.IsEnabled() {
			return func(agentruntime.TaskLaunchResult, error) {}
		}
		finishTask := coordinator.BeginTask()
		return func(result agentruntime.TaskLaunchResult, launchError error) {
			defer finishTask()
			experience, isComplete := learningExperienceFromTask(request, result, taskRunService, launchError)
			if isComplete {
				if errorValue := coordinator.Observe(experience); errorValue != nil && coordinator.Report != nil {
					coordinator.Report(errorValue)
				}
			}
		}
	}
}

type learningTaskOutcome struct {
	Status        agentcontract.TaskStatus `json:"status"`
	Result        learningText             `json:"result"`
	FailureReason learningText             `json:"failureReason"`
	FinishMessage learningText             `json:"finishMessage"`
	ToolNames     []string                 `json:"toolNames,omitempty"`
	Events        []learningEvidenceEvent  `json:"events"`
	TotalEvents   int                      `json:"totalEventCount"`
	OmittedEvents int                      `json:"omittedEventCount"`
	Incomplete    bool                     `json:"incompleteEvidence"`
	Error         string                   `json:"error,omitempty"`
}

type learningText struct {
	Value      string `json:"value,omitempty"`
	ByteLength int    `json:"byteLength"`
	SHA256     string `json:"sha256"`
	Omitted    bool   `json:"omitted"`
}

type learningEvidenceEvent struct {
	EventID     string    `json:"eventID"`
	Name        string    `json:"name"`
	CreatedAt   time.Time `json:"createdAt"`
	ByteLength  int       `json:"byteLength"`
	SHA256      string    `json:"sha256"`
	Body        string    `json:"body,omitempty"`
	BodyOmitted bool      `json:"bodyOmitted"`
}

const (
	maximumLearningEvents     = 64
	maximumLearningBody       = 2048
	maximumLearningBodyBudget = 4096
)

func learningExperienceOutcome(taskRunService *task.TaskRunService, result agentruntime.TaskLaunchResult, launchError error) json.RawMessage {
	taskRun := result.TurnResult.TaskRun
	outcome := learningTaskOutcome{
		Status:        taskRun.Status,
		Result:        learningTextOf(taskRun.Result, maximumLearningBody),
		FailureReason: learningTextOf(taskRun.FailureReason, maximumLearningBody),
		FinishMessage: learningTextOf(result.TurnResult.FinishMessage, maximumLearningBody),
		ToolNames:     append([]string{}, result.ToolNames...),
	}
	allEvents := taskRunService.ListTaskEvent(taskRun.TaskRunID)
	outcome.TotalEvents = len(allEvents)
	outcome.Events, outcome.OmittedEvents = learningEvidenceEvents(allEvents)
	outcome.Incomplete = outcome.OmittedEvents > 0 || outcome.Result.Omitted || outcome.FailureReason.Omitted || outcome.FinishMessage.Omitted
	if launchError != nil {
		outcome.Error = launchError.Error()
	}
	document, errorValue := json.Marshal(outcome)
	if errorValue != nil {
		return json.RawMessage(`{"error":"could not serialize task outcome"}`)
	}
	return document
}

func learningTextOf(value string, maximumBytes int) learningText {
	document := []byte(value)
	digest := sha256.Sum256(document)
	text := learningText{ByteLength: len(document), SHA256: fmt.Sprintf("%x", digest)}
	if len(document) > maximumBytes {
		text.Omitted = true
		return text
	}
	text.Value = value
	return text
}

func learningEvidenceEvents(events []agentcontract.TaskEvent) ([]learningEvidenceEvent, int) {
	totalOmitted := 0
	if len(events) > maximumLearningEvents {
		totalOmitted += len(events) - maximumLearningEvents
		events = events[len(events)-maximumLearningEvents:]
	}
	bounded := make([]learningEvidenceEvent, len(events))
	bodyBudget := maximumLearningBodyBudget
	for index, event := range events {
		body := learningTextOf(event.Body, maximumLearningBody)
		if !body.Omitted && len(body.Value) > bodyBudget {
			body.Value = ""
			body.Omitted = true
		}
		if !body.Omitted {
			bodyBudget -= len(body.Value)
		}
		bounded[index] = learningEvidenceEvent{
			EventID: event.TaskEventID, Name: event.Name, CreatedAt: event.CreatedAt,
			ByteLength: body.ByteLength, SHA256: body.SHA256, Body: body.Value, BodyOmitted: body.Omitted,
		}
		if body.Omitted {
			totalOmitted++
		}
	}
	return bounded, totalOmitted
}

func learningExperienceFromTask(request agentruntime.TaskLaunchRequest, result agentruntime.TaskLaunchResult, taskRunService *task.TaskRunService, launchError error) (learning.Experience, bool) {
	taskRun := result.TurnResult.TaskRun
	if strings.TrimSpace(taskRun.TaskRunID) == "" || strings.TrimSpace(request.RequesterPersonID) == "" || !learningTaskRunEnded(taskRun.Status) {
		return learning.Experience{}, false
	}
	return learning.Experience{
		TaskID:     taskRun.TaskRunID,
		Audience:   "person:" + strings.TrimSpace(request.RequesterPersonID),
		RecordedAt: time.Now().UTC(),
		Request:    request.Prompt,
		Outcome:    learningExperienceOutcome(taskRunService, result, launchError),
		Tools:      append([]string{}, result.ToolNames...),
	}, true
}

func learningTaskRunEnded(status agentcontract.TaskStatus) bool {
	return status == agentcontract.TaskStatusCompleted || status == agentcontract.TaskStatusFailed || status == agentcontract.TaskStatusCancelled
}