f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
175 lines
4.8 KiB
Go
175 lines
4.8 KiB
Go
package sync
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"go.kenn.io/agentsview/internal/parser"
|
|
)
|
|
|
|
// validateProviderOutcome rejects a provider parse outcome whose sessions,
|
|
// excluded IDs, or diagnostics do not belong to the source's agent. The engine
|
|
// runs this before applying a provider outcome so a misrouted or cross-provider
|
|
// result cannot corrupt the archive.
|
|
func validateProviderOutcome(
|
|
def parser.AgentDef,
|
|
source parser.SourceRef,
|
|
fingerprint parser.SourceFingerprint,
|
|
outcome parser.ParseOutcome,
|
|
) error {
|
|
for _, result := range outcome.Results {
|
|
session := result.Result.Session
|
|
if session.Agent != def.Type {
|
|
return fmt.Errorf(
|
|
"%s: provider result session agent mismatch for %q: got %s",
|
|
def.Type,
|
|
session.ID,
|
|
session.Agent,
|
|
)
|
|
}
|
|
if err := validateProviderParseResultSessionIDs(def, result.Result); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, sessionID := range outcome.ExcludedSessionIDs {
|
|
if err := validateProviderSessionID(def, sessionID, "excluded session id"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, sourceErr := range outcome.SourceErrors {
|
|
if err := validateProviderSessionID(def, sourceErr.SessionID, "diagnostic session id"); err != nil {
|
|
return err
|
|
}
|
|
if sourceErr.SourceKey == "" {
|
|
return fmt.Errorf(
|
|
"%s: provider diagnostic source key is required for source %q",
|
|
def.Type,
|
|
source.Key,
|
|
)
|
|
}
|
|
if !providerSourceKeyMatches(source, fingerprint, sourceErr.SourceKey) {
|
|
return fmt.Errorf(
|
|
"%s: provider diagnostic source key %q is unrelated to source %q",
|
|
def.Type,
|
|
sourceErr.SourceKey,
|
|
source.Key,
|
|
)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateProviderParseResultSessionIDs(def parser.AgentDef, result parser.ParseResult) error {
|
|
sessionIDs := []struct {
|
|
field string
|
|
id string
|
|
}{
|
|
{field: "result session id", id: result.Session.ID},
|
|
{field: "parent session id", id: result.Session.ParentSessionID},
|
|
}
|
|
for _, sessionID := range sessionIDs {
|
|
if err := validateProviderSessionID(def, sessionID.id, sessionID.field); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, usage := range result.Session.UsageEvents {
|
|
if err := validateProviderSessionID(def, usage.SessionID, "session usage event session id"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, usage := range result.UsageEvents {
|
|
if err := validateProviderSessionID(def, usage.SessionID, "usage event session id"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, message := range result.Messages {
|
|
for _, toolCall := range message.ToolCalls {
|
|
if err := validateProviderSessionID(def, toolCall.SubagentSessionID, "tool call subagent session id"); err != nil {
|
|
return err
|
|
}
|
|
for _, event := range toolCall.ResultEvents {
|
|
if err := validateProviderSessionID(def, event.SubagentSessionID, "tool result event subagent session id"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateProviderSessionID(def parser.AgentDef, sessionID, field string) error {
|
|
if sessionID == "" || def.IDPrefix == "" {
|
|
return nil
|
|
}
|
|
if strings.HasPrefix(sessionID, def.IDPrefix) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf(
|
|
"%s: provider %s %q must use prefix %q",
|
|
def.Type,
|
|
field,
|
|
sessionID,
|
|
def.IDPrefix,
|
|
)
|
|
}
|
|
|
|
func providerSourceKeyMatches(
|
|
source parser.SourceRef,
|
|
fingerprint parser.SourceFingerprint,
|
|
sourceKey string,
|
|
) bool {
|
|
if sourceKey == "" {
|
|
return true
|
|
}
|
|
for _, candidate := range []string{fingerprint.Key, source.FingerprintKey, source.Key} {
|
|
if candidate == "" {
|
|
continue
|
|
}
|
|
if sourceKey == candidate || strings.HasPrefix(sourceKey, candidate+"#") ||
|
|
strings.HasPrefix(sourceKey, candidate+"::") ||
|
|
strings.HasPrefix(sourceKey, candidate+"|") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// parseOutcomeResults flattens a provider parse outcome's per-result wrappers
|
|
// into the bare ParseResults the engine writes.
|
|
func parseOutcomeResults(outcomes []parser.ParseResultOutcome) []parser.ParseResult {
|
|
results := make([]parser.ParseResult, 0, len(outcomes))
|
|
for _, outcome := range outcomes {
|
|
results = append(results, outcome.Result)
|
|
}
|
|
return results
|
|
}
|
|
|
|
// plannedSourceKey is the stable identity the engine uses for a provider source
|
|
// when recording source-level state. It prefers the fingerprint key, then the
|
|
// source's own keys.
|
|
func plannedSourceKey(
|
|
source parser.SourceRef,
|
|
fingerprint parser.SourceFingerprint,
|
|
) string {
|
|
if fingerprint.Key != "" {
|
|
return fingerprint.Key
|
|
}
|
|
if source.FingerprintKey != "" {
|
|
return source.FingerprintKey
|
|
}
|
|
return source.Key
|
|
}
|
|
|
|
// plannedSkipKey is the skip-cache key the engine stores for a provider source
|
|
// that parsed to no work. It prefers the source fingerprint key so the skip
|
|
// entry keys off the same identity used elsewhere.
|
|
func plannedSkipKey(
|
|
source parser.SourceRef,
|
|
fingerprint parser.SourceFingerprint,
|
|
) string {
|
|
if source.FingerprintKey != "" {
|
|
return source.FingerprintKey
|
|
}
|
|
return plannedSourceKey(source, fingerprint)
|
|
}
|