Decompose analyze artifact execution
This commit is contained in:
@@ -121,7 +121,7 @@ During every stage:
|
||||
|
||||
## Stage 1 — Decompose The Single-Artifact Transaction
|
||||
|
||||
**Status:** Pending
|
||||
**Status:** Completed
|
||||
|
||||
### Goal
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@ package stage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
@@ -11,7 +9,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/adapters/scriptorium"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||
@@ -491,266 +488,6 @@ func cloneAnalyzeOutputContract(value *artifactmodel.ContractMetadata) *artifact
|
||||
return &cloned
|
||||
}
|
||||
|
||||
func executeAnalyzeArtifact(
|
||||
ctx context.Context,
|
||||
execution analyzeExecutionContext,
|
||||
plan analyzeArtifactExecutionPlan,
|
||||
) (*analyzeArtifactExecutionResult, error) {
|
||||
env := execution.Env
|
||||
paths := execution.Paths
|
||||
runLayout := execution.RunLayout
|
||||
sessionID := execution.SessionID
|
||||
transcriptRefs := execution.TranscriptRefs
|
||||
artifactName := plan.Name
|
||||
artifactCfg := plan.Cfg
|
||||
|
||||
resolvedInputs, err := resolveAnalyzeInputIdentities(artifactCfg.Inputs, execution)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyze: resolve inputs for artifact %q: %w", artifactName, err)
|
||||
}
|
||||
inputPaths := resolvedInputs.Paths()
|
||||
omittedOptionalInputs := []string{}
|
||||
reusedArtifacts := []map[string]any{}
|
||||
for _, identity := range resolvedInputs.Ordered {
|
||||
if !identity.Present {
|
||||
omittedOptionalInputs = append(omittedOptionalInputs, identity.Name)
|
||||
continue
|
||||
}
|
||||
resolvedArtifact := resolvedInputs.Artifact(identity.Name)
|
||||
if resolvedArtifact != nil && resolvedArtifact.Provenance == artifacts.ArtifactProvenanceCurrentAnalyzeManifest {
|
||||
reusedArtifacts = append(reusedArtifacts, map[string]any{
|
||||
"name": configuredArtifactNameFromSourceID(resolvedArtifact.ID),
|
||||
"source_id": resolvedArtifact.ID,
|
||||
"path": resolvedArtifact.Path,
|
||||
"provenance": resolvedArtifact.Provenance,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
vars, err := buildScriptoriumVars(artifactCfg.Vars, env.Config.Session)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyze: resolve vars for artifact %q: %w", artifactName, err)
|
||||
}
|
||||
vars = withScriptoriumStickySessionVar(vars, sessionID)
|
||||
|
||||
canonicalOutputPath, err := resolveScriptoriumOutputPath(paths, artifactCfg.OutputPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyze: resolve output path for artifact %q: %w", artifactName, err)
|
||||
}
|
||||
outputPath, err := runLocalPathForCanonical(runLayout, paths, canonicalOutputPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyze: resolve run-local output path for artifact %q: %w", artifactName, err)
|
||||
}
|
||||
|
||||
stdoutLogPath := filepath.Join(paths.LogsDir, "scriptorium."+artifactName+".stdout.log")
|
||||
stderrLogPath := filepath.Join(paths.LogsDir, "scriptorium."+artifactName+".stderr.log")
|
||||
generatedConfigPath := filepath.Join(paths.ConfigDir, "scriptorium."+artifactName+".generated.yml")
|
||||
if runLayout.Enabled {
|
||||
stdoutLogPath = filepath.Join(runLayout.LogsDir, "scriptorium."+artifactName+".stdout.log")
|
||||
stderrLogPath = filepath.Join(runLayout.LogsDir, "scriptorium."+artifactName+".stderr.log")
|
||||
generatedConfigPath = filepath.Join(runLayout.ConfigDir, "scriptorium."+artifactName+".generated.yml")
|
||||
}
|
||||
|
||||
timeout, err := resolveScriptoriumTimeout(env.Config.Pipeline.Scriptorium.Timeout, artifactCfg.Timeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyze: resolve timeout for artifact %q: %w", artifactName, err)
|
||||
}
|
||||
|
||||
logPaths := []string{}
|
||||
generatedConfigs := []string{}
|
||||
meta := map[string]any{
|
||||
"stage": "analyze",
|
||||
"name": artifactName,
|
||||
"artifact_name": artifactName,
|
||||
"source_id": artifacts.ConfiguredArtifactSourceID(artifactName),
|
||||
"output_kind": "scriptorium_artifact",
|
||||
"prompt_id": artifactCfg.PromptID,
|
||||
"profile_id": artifactCfg.ProfileID,
|
||||
"binary": env.Config.Pipeline.Scriptorium.Binary,
|
||||
"config_path": env.Config.Pipeline.Scriptorium.ConfigPath,
|
||||
"input_paths": inputPaths,
|
||||
"input_names": sortedMapKeys(inputPaths),
|
||||
"omitted_optional_inputs": omittedOptionalInputs,
|
||||
"vars": vars,
|
||||
"timeout": timeout.String(),
|
||||
"processed_transcript_path": transcriptRefs.ProcessedPath,
|
||||
"processed_transcript_source": transcriptRefs.ProcessedSource,
|
||||
"normalized_transcript_path": transcriptRefs.NormalizedPath,
|
||||
"normalized_transcript_source": transcriptRefs.NormalizedSource,
|
||||
"trimmed_transcript_path": transcriptRefs.TrimmedPath,
|
||||
"trimmed_transcript_source": transcriptRefs.TrimmedSource,
|
||||
"render_debug_enabled": resolveRenderDebugEnabled(env.Config.Pipeline.Scriptorium.RenderDebug, artifactCfg.RenderDebug),
|
||||
}
|
||||
|
||||
if meta["render_debug_enabled"] == true {
|
||||
renderOutputPath := filepath.Join(paths.ArtifactsDir, artifactName+".render.json")
|
||||
if runLayout.Enabled {
|
||||
renderOutputPath = filepath.Join(runLayout.ReportsDir, artifactName+".render.json")
|
||||
}
|
||||
renderStdoutPath := filepath.Join(paths.LogsDir, "scriptorium."+artifactName+".render.stdout.log")
|
||||
renderStderrPath := filepath.Join(paths.LogsDir, "scriptorium."+artifactName+".render.stderr.log")
|
||||
renderGeneratedConfigPath := filepath.Join(paths.ConfigDir, "scriptorium."+artifactName+".render.generated.yml")
|
||||
if runLayout.Enabled {
|
||||
renderStdoutPath = filepath.Join(runLayout.LogsDir, "scriptorium."+artifactName+".render.stdout.log")
|
||||
renderStderrPath = filepath.Join(runLayout.LogsDir, "scriptorium."+artifactName+".render.stderr.log")
|
||||
renderGeneratedConfigPath = filepath.Join(runLayout.ConfigDir, "scriptorium."+artifactName+".render.generated.yml")
|
||||
}
|
||||
|
||||
renderReq := scriptorium.RenderArtifactRequest{
|
||||
Binary: env.Config.Pipeline.Scriptorium.Binary,
|
||||
ConfigPath: env.Config.Pipeline.Scriptorium.ConfigPath,
|
||||
PromptID: artifactCfg.PromptID,
|
||||
ProfileID: artifactCfg.ProfileID,
|
||||
InputPaths: inputPaths,
|
||||
Vars: vars,
|
||||
OutputPath: renderOutputPath,
|
||||
StdoutLogPath: renderStdoutPath,
|
||||
StderrLogPath: renderStderrPath,
|
||||
GeneratedConfigPath: renderGeneratedConfigPath,
|
||||
Timeout: timeout,
|
||||
}
|
||||
renderRes, renderErr := env.Scriptorium.RenderArtifact(ctx, renderReq)
|
||||
if renderErr != nil {
|
||||
return nil, fmt.Errorf("analyze: scriptorium render failed for artifact %q: %w", artifactName, renderErr)
|
||||
}
|
||||
if renderRes.ValidationFailed {
|
||||
return nil, fmt.Errorf("analyze: scriptorium render returned validation_failed=true for artifact %q", artifactName)
|
||||
}
|
||||
finalRenderOutputPath, err := authoritativeOutputPath(renderReq.OutputPath, renderRes.OutputPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyze: %w", err)
|
||||
}
|
||||
if err := requireNonEmptyFile(finalRenderOutputPath, artifactName+" render output"); err != nil {
|
||||
return nil, fmt.Errorf("analyze: %w", err)
|
||||
}
|
||||
if err := validateJSONFile(finalRenderOutputPath); err != nil {
|
||||
return nil, fmt.Errorf("analyze: render diagnostics %q invalid json: %w", finalRenderOutputPath, err)
|
||||
}
|
||||
|
||||
logPaths = append(logPaths, renderStdoutPath, renderStderrPath)
|
||||
generatedConfigs = append(generatedConfigs, renderGeneratedConfigPath)
|
||||
meta["render_output_path"] = finalRenderOutputPath
|
||||
meta["render_stdout_log_path"] = renderStdoutPath
|
||||
meta["render_stderr_log_path"] = renderStderrPath
|
||||
meta["render_generated_config_path"] = renderGeneratedConfigPath
|
||||
meta["render_adapter_exit_code"] = renderRes.ExitCode
|
||||
meta["render_adapter_duration_ms"] = renderRes.Duration.Milliseconds()
|
||||
meta["render_adapter_command_mode"] = renderRes.CommandMode
|
||||
meta["render_adapter_prompt_id"] = renderRes.PromptID
|
||||
meta["render_adapter_profile_id"] = renderRes.ProfileID
|
||||
meta["render_adapter_output_path"] = renderRes.OutputPath
|
||||
meta["render_adapter_generated_config"] = renderRes.GeneratedConfigPath
|
||||
meta["render_adapter_stdout_log_path"] = renderRes.StdoutLogPath
|
||||
meta["render_adapter_stderr_log_path"] = renderRes.StderrLogPath
|
||||
if renderRes.Metadata != nil {
|
||||
meta["render_adapter_metadata"] = renderRes.Metadata
|
||||
}
|
||||
}
|
||||
|
||||
req := scriptorium.RunArtifactRequest{
|
||||
Binary: env.Config.Pipeline.Scriptorium.Binary,
|
||||
ConfigPath: env.Config.Pipeline.Scriptorium.ConfigPath,
|
||||
PromptID: artifactCfg.PromptID,
|
||||
ProfileID: artifactCfg.ProfileID,
|
||||
InputPaths: inputPaths,
|
||||
Vars: vars,
|
||||
OutputPath: outputPath,
|
||||
StdoutLogPath: stdoutLogPath,
|
||||
StderrLogPath: stderrLogPath,
|
||||
GeneratedConfigPath: generatedConfigPath,
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
res, runErr := env.Scriptorium.RunArtifact(ctx, req)
|
||||
if runErr != nil {
|
||||
if res.ValidationFailed {
|
||||
return nil, fmt.Errorf(
|
||||
"analyze: scriptorium validation failed (artifact=%q, prompt_id=%q, output_path=%q, exit_code=%d, stdout_log=%q, stderr_log=%q): %w",
|
||||
artifactName,
|
||||
req.PromptID,
|
||||
req.OutputPath,
|
||||
res.ExitCode,
|
||||
coalesceString(res.StdoutLogPath, req.StdoutLogPath),
|
||||
coalesceString(res.StderrLogPath, req.StderrLogPath),
|
||||
runErr,
|
||||
)
|
||||
}
|
||||
return nil, fmt.Errorf("analyze: scriptorium run failed for artifact %q: %w", artifactName, runErr)
|
||||
}
|
||||
if res.ValidationFailed {
|
||||
return nil, fmt.Errorf("analyze: scriptorium run returned validation_failed=true for artifact %q", artifactName)
|
||||
}
|
||||
|
||||
finalOutputPath, err := authoritativeOutputPath(req.OutputPath, res.OutputPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyze: %w", err)
|
||||
}
|
||||
if err := requireNonEmptyFile(finalOutputPath, artifactName+" output"); err != nil {
|
||||
return nil, fmt.Errorf("analyze: %w", err)
|
||||
}
|
||||
validatedOutput, err := readExternalResult(finalOutputPath, artifactName+" output")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyze: %w", err)
|
||||
}
|
||||
contract := &artifactmodel.ContractMetadata{
|
||||
MediaType: "text/markdown", SchemaID: "narratio." + artifactName, SchemaVersion: "1",
|
||||
}
|
||||
relativeOutputPath, err := normalizedAnalyzeOutputIdentity(artifactCfg.OutputPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyze: normalize output identity for artifact %q: %w", artifactName, err)
|
||||
}
|
||||
materializedArtifact, err := materializeRunLocalOutput(env.ArtifactStore, finalOutputPath, canonicalOutputPath, artifacts.Ref{
|
||||
Kind: artifactName,
|
||||
SourceID: artifacts.ConfiguredArtifactSourceID(artifactName),
|
||||
Category: "artifacts",
|
||||
SessionID: sessionID,
|
||||
RelativePath: relativeOutputPath,
|
||||
Contract: contract,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyze: materialize artifact output for %q: %w", artifactName, err)
|
||||
}
|
||||
expectedDigest := sha256.Sum256(validatedOutput)
|
||||
if materializedArtifact.Checksum != hex.EncodeToString(expectedDigest[:]) {
|
||||
return nil, fmt.Errorf("analyze: materialized artifact output for %q differs from validated run-local bytes", artifactName)
|
||||
}
|
||||
|
||||
logPaths = append(logPaths, stdoutLogPath, stderrLogPath)
|
||||
generatedConfigs = append(generatedConfigs, generatedConfigPath)
|
||||
meta["run_output_path"] = finalOutputPath
|
||||
meta["path"] = canonicalOutputPath
|
||||
meta["output_path"] = canonicalOutputPath
|
||||
meta["generated_config_path"] = generatedConfigPath
|
||||
meta["stdout_log_path"] = stdoutLogPath
|
||||
meta["stderr_log_path"] = stderrLogPath
|
||||
meta["adapter_exit_code"] = res.ExitCode
|
||||
meta["adapter_duration_ms"] = res.Duration.Milliseconds()
|
||||
meta["adapter_command_mode"] = res.CommandMode
|
||||
meta["adapter_prompt_id"] = res.PromptID
|
||||
meta["adapter_profile_id"] = res.ProfileID
|
||||
meta["adapter_validation_failed"] = res.ValidationFailed
|
||||
meta["adapter_output_path"] = res.OutputPath
|
||||
meta["adapter_generated_config"] = res.GeneratedConfigPath
|
||||
meta["adapter_stdout_log_path"] = res.StdoutLogPath
|
||||
meta["adapter_stderr_log_path"] = res.StderrLogPath
|
||||
meta["provenance"] = artifacts.ArtifactProvenanceGeneratedCurrentAnalyzeRun
|
||||
if res.Metadata != nil {
|
||||
meta["adapter_metadata"] = res.Metadata
|
||||
}
|
||||
|
||||
return &analyzeArtifactExecutionResult{
|
||||
Output: materializedArtifact,
|
||||
OutputSize: int64(len(validatedOutput)),
|
||||
Scriptorium: manifest.AnalyzeArtifactProvenance{
|
||||
PromptID: artifactCfg.PromptID, ProfileID: artifactCfg.ProfileID, CommandMode: res.CommandMode,
|
||||
},
|
||||
Logs: logPaths,
|
||||
GeneratedConfigs: generatedConfigs,
|
||||
Metadata: meta,
|
||||
ReusedArtifacts: reusedArtifacts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func configuredArtifactNameFromSourceID(sourceID string) string {
|
||||
name, _ := artifactpolicy.ParseConfiguredSource(sourceID)
|
||||
return name
|
||||
|
||||
385
internal/stage/analyze_artifact_execution.go
Normal file
385
internal/stage/analyze_artifact_execution.go
Normal file
@@ -0,0 +1,385 @@
|
||||
package stage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/adapters/scriptorium"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||
)
|
||||
|
||||
type analyzeArtifactPreparation struct {
|
||||
name string
|
||||
promptID string
|
||||
profileID string
|
||||
inputPaths map[string]string
|
||||
vars map[string]string
|
||||
timeout time.Duration
|
||||
canonicalOutputPath string
|
||||
runOutputPath string
|
||||
stdoutLogPath string
|
||||
stderrLogPath string
|
||||
generatedConfigPath string
|
||||
renderEnabled bool
|
||||
renderOutputPath string
|
||||
renderStdoutPath string
|
||||
renderStderrPath string
|
||||
renderConfigPath string
|
||||
metadata map[string]any
|
||||
reusedArtifacts []map[string]any
|
||||
logs []string
|
||||
generatedConfigs []string
|
||||
}
|
||||
|
||||
type analyzeArtifactInvocation struct {
|
||||
request scriptorium.RunArtifactRequest
|
||||
result scriptorium.ArtifactResult
|
||||
}
|
||||
|
||||
type analyzeArtifactMaterialization struct {
|
||||
output artifacts.Ref
|
||||
outputSize int64
|
||||
runOutputPath string
|
||||
}
|
||||
|
||||
func executeAnalyzeArtifact(
|
||||
ctx context.Context,
|
||||
execution analyzeExecutionContext,
|
||||
plan analyzeArtifactExecutionPlan,
|
||||
) (*analyzeArtifactExecutionResult, error) {
|
||||
prepared, err := prepareAnalyzeArtifact(execution, plan)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := invokeAnalyzeRender(ctx, execution, prepared); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
invocation, err := invokeAnalyzeArtifact(ctx, execution, prepared)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
materialized, err := validateAndMaterializeAnalyzeArtifact(execution, plan, prepared, invocation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buildAnalyzeArtifactExecutionResult(plan, prepared, invocation, materialized), nil
|
||||
}
|
||||
|
||||
func prepareAnalyzeArtifact(
|
||||
execution analyzeExecutionContext,
|
||||
plan analyzeArtifactExecutionPlan,
|
||||
) (*analyzeArtifactPreparation, error) {
|
||||
env := execution.Env
|
||||
paths := execution.Paths
|
||||
runLayout := execution.RunLayout
|
||||
artifactName := plan.Name
|
||||
artifactCfg := plan.Cfg
|
||||
|
||||
resolvedInputs, err := resolveAnalyzeInputIdentities(artifactCfg.Inputs, execution)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyze: resolve inputs for artifact %q: %w", artifactName, err)
|
||||
}
|
||||
inputPaths := resolvedInputs.Paths()
|
||||
omittedOptionalInputs := []string{}
|
||||
reusedArtifacts := []map[string]any{}
|
||||
for _, identity := range resolvedInputs.Ordered {
|
||||
if !identity.Present {
|
||||
omittedOptionalInputs = append(omittedOptionalInputs, identity.Name)
|
||||
continue
|
||||
}
|
||||
resolvedArtifact := resolvedInputs.Artifact(identity.Name)
|
||||
if resolvedArtifact != nil && resolvedArtifact.Provenance == artifacts.ArtifactProvenanceCurrentAnalyzeManifest {
|
||||
reusedArtifacts = append(reusedArtifacts, map[string]any{
|
||||
"name": configuredArtifactNameFromSourceID(resolvedArtifact.ID),
|
||||
"source_id": resolvedArtifact.ID,
|
||||
"path": resolvedArtifact.Path,
|
||||
"provenance": resolvedArtifact.Provenance,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
vars, err := buildScriptoriumVars(artifactCfg.Vars, env.Config.Session)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyze: resolve vars for artifact %q: %w", artifactName, err)
|
||||
}
|
||||
vars = withScriptoriumStickySessionVar(vars, execution.SessionID)
|
||||
|
||||
canonicalOutputPath, err := resolveScriptoriumOutputPath(paths, artifactCfg.OutputPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyze: resolve output path for artifact %q: %w", artifactName, err)
|
||||
}
|
||||
runOutputPath, err := runLocalPathForCanonical(runLayout, paths, canonicalOutputPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyze: resolve run-local output path for artifact %q: %w", artifactName, err)
|
||||
}
|
||||
|
||||
stdoutLogPath := filepath.Join(paths.LogsDir, "scriptorium."+artifactName+".stdout.log")
|
||||
stderrLogPath := filepath.Join(paths.LogsDir, "scriptorium."+artifactName+".stderr.log")
|
||||
generatedConfigPath := filepath.Join(paths.ConfigDir, "scriptorium."+artifactName+".generated.yml")
|
||||
renderOutputPath := filepath.Join(paths.ArtifactsDir, artifactName+".render.json")
|
||||
renderStdoutPath := filepath.Join(paths.LogsDir, "scriptorium."+artifactName+".render.stdout.log")
|
||||
renderStderrPath := filepath.Join(paths.LogsDir, "scriptorium."+artifactName+".render.stderr.log")
|
||||
renderConfigPath := filepath.Join(paths.ConfigDir, "scriptorium."+artifactName+".render.generated.yml")
|
||||
if runLayout.Enabled {
|
||||
stdoutLogPath = filepath.Join(runLayout.LogsDir, "scriptorium."+artifactName+".stdout.log")
|
||||
stderrLogPath = filepath.Join(runLayout.LogsDir, "scriptorium."+artifactName+".stderr.log")
|
||||
generatedConfigPath = filepath.Join(runLayout.ConfigDir, "scriptorium."+artifactName+".generated.yml")
|
||||
renderOutputPath = filepath.Join(runLayout.ReportsDir, artifactName+".render.json")
|
||||
renderStdoutPath = filepath.Join(runLayout.LogsDir, "scriptorium."+artifactName+".render.stdout.log")
|
||||
renderStderrPath = filepath.Join(runLayout.LogsDir, "scriptorium."+artifactName+".render.stderr.log")
|
||||
renderConfigPath = filepath.Join(runLayout.ConfigDir, "scriptorium."+artifactName+".render.generated.yml")
|
||||
}
|
||||
|
||||
timeout, err := resolveScriptoriumTimeout(env.Config.Pipeline.Scriptorium.Timeout, artifactCfg.Timeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyze: resolve timeout for artifact %q: %w", artifactName, err)
|
||||
}
|
||||
renderEnabled := resolveRenderDebugEnabled(env.Config.Pipeline.Scriptorium.RenderDebug, artifactCfg.RenderDebug)
|
||||
|
||||
return &analyzeArtifactPreparation{
|
||||
name: artifactName,
|
||||
promptID: artifactCfg.PromptID,
|
||||
profileID: artifactCfg.ProfileID,
|
||||
inputPaths: inputPaths,
|
||||
vars: vars,
|
||||
timeout: timeout,
|
||||
canonicalOutputPath: canonicalOutputPath,
|
||||
runOutputPath: runOutputPath,
|
||||
stdoutLogPath: stdoutLogPath,
|
||||
stderrLogPath: stderrLogPath,
|
||||
generatedConfigPath: generatedConfigPath,
|
||||
renderEnabled: renderEnabled,
|
||||
renderOutputPath: renderOutputPath,
|
||||
renderStdoutPath: renderStdoutPath,
|
||||
renderStderrPath: renderStderrPath,
|
||||
renderConfigPath: renderConfigPath,
|
||||
reusedArtifacts: reusedArtifacts,
|
||||
metadata: map[string]any{
|
||||
"stage": "analyze",
|
||||
"name": artifactName,
|
||||
"artifact_name": artifactName,
|
||||
"source_id": artifacts.ConfiguredArtifactSourceID(artifactName),
|
||||
"output_kind": "scriptorium_artifact",
|
||||
"prompt_id": artifactCfg.PromptID,
|
||||
"profile_id": artifactCfg.ProfileID,
|
||||
"binary": env.Config.Pipeline.Scriptorium.Binary,
|
||||
"config_path": env.Config.Pipeline.Scriptorium.ConfigPath,
|
||||
"input_paths": inputPaths,
|
||||
"input_names": sortedMapKeys(inputPaths),
|
||||
"omitted_optional_inputs": omittedOptionalInputs,
|
||||
"vars": vars,
|
||||
"timeout": timeout.String(),
|
||||
"processed_transcript_path": execution.TranscriptRefs.ProcessedPath,
|
||||
"processed_transcript_source": execution.TranscriptRefs.ProcessedSource,
|
||||
"normalized_transcript_path": execution.TranscriptRefs.NormalizedPath,
|
||||
"normalized_transcript_source": execution.TranscriptRefs.NormalizedSource,
|
||||
"trimmed_transcript_path": execution.TranscriptRefs.TrimmedPath,
|
||||
"trimmed_transcript_source": execution.TranscriptRefs.TrimmedSource,
|
||||
"render_debug_enabled": renderEnabled,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func invokeAnalyzeRender(
|
||||
ctx context.Context,
|
||||
execution analyzeExecutionContext,
|
||||
prepared *analyzeArtifactPreparation,
|
||||
) error {
|
||||
if !prepared.renderEnabled {
|
||||
return nil
|
||||
}
|
||||
req := scriptorium.RenderArtifactRequest{
|
||||
Binary: execution.Env.Config.Pipeline.Scriptorium.Binary,
|
||||
ConfigPath: execution.Env.Config.Pipeline.Scriptorium.ConfigPath,
|
||||
PromptID: prepared.promptID,
|
||||
ProfileID: prepared.profileID,
|
||||
InputPaths: prepared.inputPaths,
|
||||
Vars: prepared.vars,
|
||||
OutputPath: prepared.renderOutputPath,
|
||||
StdoutLogPath: prepared.renderStdoutPath,
|
||||
StderrLogPath: prepared.renderStderrPath,
|
||||
GeneratedConfigPath: prepared.renderConfigPath,
|
||||
Timeout: prepared.timeout,
|
||||
}
|
||||
res, err := execution.Env.Scriptorium.RenderArtifact(ctx, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("analyze: scriptorium render failed for artifact %q: %w", prepared.name, err)
|
||||
}
|
||||
if res.ValidationFailed {
|
||||
return fmt.Errorf("analyze: scriptorium render returned validation_failed=true for artifact %q", prepared.name)
|
||||
}
|
||||
outputPath, err := authoritativeOutputPath(req.OutputPath, res.OutputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("analyze: %w", err)
|
||||
}
|
||||
if err := requireNonEmptyFile(outputPath, prepared.name+" render output"); err != nil {
|
||||
return fmt.Errorf("analyze: %w", err)
|
||||
}
|
||||
if err := validateJSONFile(outputPath); err != nil {
|
||||
return fmt.Errorf("analyze: render diagnostics %q invalid json: %w", outputPath, err)
|
||||
}
|
||||
|
||||
prepared.logs = append(prepared.logs, prepared.renderStdoutPath, prepared.renderStderrPath)
|
||||
prepared.generatedConfigs = append(prepared.generatedConfigs, prepared.renderConfigPath)
|
||||
prepared.metadata["render_output_path"] = outputPath
|
||||
prepared.metadata["render_stdout_log_path"] = prepared.renderStdoutPath
|
||||
prepared.metadata["render_stderr_log_path"] = prepared.renderStderrPath
|
||||
prepared.metadata["render_generated_config_path"] = prepared.renderConfigPath
|
||||
prepared.metadata["render_adapter_exit_code"] = res.ExitCode
|
||||
prepared.metadata["render_adapter_duration_ms"] = res.Duration.Milliseconds()
|
||||
prepared.metadata["render_adapter_command_mode"] = res.CommandMode
|
||||
prepared.metadata["render_adapter_prompt_id"] = res.PromptID
|
||||
prepared.metadata["render_adapter_profile_id"] = res.ProfileID
|
||||
prepared.metadata["render_adapter_output_path"] = res.OutputPath
|
||||
prepared.metadata["render_adapter_generated_config"] = res.GeneratedConfigPath
|
||||
prepared.metadata["render_adapter_stdout_log_path"] = res.StdoutLogPath
|
||||
prepared.metadata["render_adapter_stderr_log_path"] = res.StderrLogPath
|
||||
if res.Metadata != nil {
|
||||
prepared.metadata["render_adapter_metadata"] = res.Metadata
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func invokeAnalyzeArtifact(
|
||||
ctx context.Context,
|
||||
execution analyzeExecutionContext,
|
||||
prepared *analyzeArtifactPreparation,
|
||||
) (analyzeArtifactInvocation, error) {
|
||||
req := scriptorium.RunArtifactRequest{
|
||||
Binary: execution.Env.Config.Pipeline.Scriptorium.Binary,
|
||||
ConfigPath: execution.Env.Config.Pipeline.Scriptorium.ConfigPath,
|
||||
PromptID: prepared.promptID,
|
||||
ProfileID: prepared.profileID,
|
||||
InputPaths: prepared.inputPaths,
|
||||
Vars: prepared.vars,
|
||||
OutputPath: prepared.runOutputPath,
|
||||
StdoutLogPath: prepared.stdoutLogPath,
|
||||
StderrLogPath: prepared.stderrLogPath,
|
||||
GeneratedConfigPath: prepared.generatedConfigPath,
|
||||
Timeout: prepared.timeout,
|
||||
}
|
||||
res, err := execution.Env.Scriptorium.RunArtifact(ctx, req)
|
||||
invocation := analyzeArtifactInvocation{request: req, result: res}
|
||||
if err != nil {
|
||||
if res.ValidationFailed {
|
||||
return invocation, fmt.Errorf(
|
||||
"analyze: scriptorium validation failed (artifact=%q, prompt_id=%q, output_path=%q, exit_code=%d, stdout_log=%q, stderr_log=%q): %w",
|
||||
prepared.name,
|
||||
req.PromptID,
|
||||
req.OutputPath,
|
||||
res.ExitCode,
|
||||
coalesceString(res.StdoutLogPath, req.StdoutLogPath),
|
||||
coalesceString(res.StderrLogPath, req.StderrLogPath),
|
||||
err,
|
||||
)
|
||||
}
|
||||
return invocation, fmt.Errorf("analyze: scriptorium run failed for artifact %q: %w", prepared.name, err)
|
||||
}
|
||||
if res.ValidationFailed {
|
||||
return invocation, fmt.Errorf("analyze: scriptorium run returned validation_failed=true for artifact %q", prepared.name)
|
||||
}
|
||||
return invocation, nil
|
||||
}
|
||||
|
||||
func validateAndMaterializeAnalyzeArtifact(
|
||||
execution analyzeExecutionContext,
|
||||
plan analyzeArtifactExecutionPlan,
|
||||
prepared *analyzeArtifactPreparation,
|
||||
invocation analyzeArtifactInvocation,
|
||||
) (analyzeArtifactMaterialization, error) {
|
||||
outputPath, err := authoritativeOutputPath(invocation.request.OutputPath, invocation.result.OutputPath)
|
||||
if err != nil {
|
||||
return analyzeArtifactMaterialization{}, fmt.Errorf("analyze: %w", err)
|
||||
}
|
||||
if err := requireNonEmptyFile(outputPath, prepared.name+" output"); err != nil {
|
||||
return analyzeArtifactMaterialization{}, fmt.Errorf("analyze: %w", err)
|
||||
}
|
||||
validatedOutput, err := readExternalResult(outputPath, prepared.name+" output")
|
||||
if err != nil {
|
||||
return analyzeArtifactMaterialization{}, fmt.Errorf("analyze: %w", err)
|
||||
}
|
||||
contract := &artifactmodel.ContractMetadata{
|
||||
MediaType: "text/markdown", SchemaID: "narratio." + prepared.name, SchemaVersion: "1",
|
||||
}
|
||||
relativeOutputPath, err := normalizedAnalyzeOutputIdentity(plan.Cfg.OutputPath)
|
||||
if err != nil {
|
||||
return analyzeArtifactMaterialization{}, fmt.Errorf("analyze: normalize output identity for artifact %q: %w", prepared.name, err)
|
||||
}
|
||||
materialized, err := materializeRunLocalOutput(
|
||||
execution.Env.ArtifactStore,
|
||||
outputPath,
|
||||
prepared.canonicalOutputPath,
|
||||
artifacts.Ref{
|
||||
Kind: prepared.name,
|
||||
SourceID: artifacts.ConfiguredArtifactSourceID(prepared.name),
|
||||
Category: "artifacts",
|
||||
SessionID: execution.SessionID,
|
||||
RelativePath: relativeOutputPath,
|
||||
Contract: contract,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return analyzeArtifactMaterialization{}, fmt.Errorf("analyze: materialize artifact output for %q: %w", prepared.name, err)
|
||||
}
|
||||
expectedDigest := sha256.Sum256(validatedOutput)
|
||||
if materialized.Checksum != hex.EncodeToString(expectedDigest[:]) {
|
||||
return analyzeArtifactMaterialization{}, fmt.Errorf(
|
||||
"analyze: materialized artifact output for %q differs from validated run-local bytes",
|
||||
prepared.name,
|
||||
)
|
||||
}
|
||||
return analyzeArtifactMaterialization{
|
||||
output: materialized,
|
||||
outputSize: int64(len(validatedOutput)),
|
||||
runOutputPath: outputPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildAnalyzeArtifactExecutionResult(
|
||||
plan analyzeArtifactExecutionPlan,
|
||||
prepared *analyzeArtifactPreparation,
|
||||
invocation analyzeArtifactInvocation,
|
||||
materialized analyzeArtifactMaterialization,
|
||||
) *analyzeArtifactExecutionResult {
|
||||
res := invocation.result
|
||||
prepared.logs = append(prepared.logs, prepared.stdoutLogPath, prepared.stderrLogPath)
|
||||
prepared.generatedConfigs = append(prepared.generatedConfigs, prepared.generatedConfigPath)
|
||||
prepared.metadata["run_output_path"] = materialized.runOutputPath
|
||||
prepared.metadata["path"] = prepared.canonicalOutputPath
|
||||
prepared.metadata["output_path"] = prepared.canonicalOutputPath
|
||||
prepared.metadata["generated_config_path"] = prepared.generatedConfigPath
|
||||
prepared.metadata["stdout_log_path"] = prepared.stdoutLogPath
|
||||
prepared.metadata["stderr_log_path"] = prepared.stderrLogPath
|
||||
prepared.metadata["adapter_exit_code"] = res.ExitCode
|
||||
prepared.metadata["adapter_duration_ms"] = res.Duration.Milliseconds()
|
||||
prepared.metadata["adapter_command_mode"] = res.CommandMode
|
||||
prepared.metadata["adapter_prompt_id"] = res.PromptID
|
||||
prepared.metadata["adapter_profile_id"] = res.ProfileID
|
||||
prepared.metadata["adapter_validation_failed"] = res.ValidationFailed
|
||||
prepared.metadata["adapter_output_path"] = res.OutputPath
|
||||
prepared.metadata["adapter_generated_config"] = res.GeneratedConfigPath
|
||||
prepared.metadata["adapter_stdout_log_path"] = res.StdoutLogPath
|
||||
prepared.metadata["adapter_stderr_log_path"] = res.StderrLogPath
|
||||
prepared.metadata["provenance"] = artifacts.ArtifactProvenanceGeneratedCurrentAnalyzeRun
|
||||
if res.Metadata != nil {
|
||||
prepared.metadata["adapter_metadata"] = res.Metadata
|
||||
}
|
||||
|
||||
return &analyzeArtifactExecutionResult{
|
||||
Output: materialized.output,
|
||||
OutputSize: materialized.outputSize,
|
||||
Scriptorium: manifest.AnalyzeArtifactProvenance{
|
||||
PromptID: plan.Cfg.PromptID, ProfileID: plan.Cfg.ProfileID, CommandMode: res.CommandMode,
|
||||
},
|
||||
Logs: prepared.logs,
|
||||
GeneratedConfigs: prepared.generatedConfigs,
|
||||
Metadata: prepared.metadata,
|
||||
ReusedArtifacts: prepared.reusedArtifacts,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user