| 461 | } |
| 462 | |
| 463 | func (c *Compiler) generatePrompt(yaml *strings.Builder, data *WorkflowData, preActivationJobCreated bool, beforeActivationJobs []string) { |
| 464 | compilerYamlLog.Printf("Generating prompt for workflow: %s (markdown size: %d bytes)", data.Name, len(data.MarkdownContent)) |
| 465 | |
| 466 | // Collect built-in prompt sections (these should be prepended to user prompt) |
| 467 | builtinSections := c.collectPromptSections(data) |
| 468 | compilerYamlLog.Printf("Collected %d built-in prompt sections", len(builtinSections)) |
| 469 | |
| 470 | // NEW APPROACH: Use runtime-import macros for imports without inputs |
| 471 | // - Imported markdown without inputs uses runtime-import macros (loaded at runtime) |
| 472 | // - Imported markdown with inputs is still inlined (compile-time substitution required) |
| 473 | // - Main workflow markdown body uses runtime-import to allow editing without recompilation |
| 474 | // This ensures consistency for most imports while maintaining import inputs functionality |
| 475 | // |
| 476 | // NOTE: When an engine does not support native agent-file handling |
| 477 | // (GetCapabilities().NativeAgentFile == false), the agent file content is already present in the |
| 478 | // prompt via the standard mechanisms below — no special Step 0 is needed: |
| 479 | // - Agent files WITHOUT inputs: path is in data.ImportPaths → included by Step 1b. |
| 480 | // - Agent files WITH inputs: content is in data.ImportedMarkdown → included by Step 1a. |
| 481 | // - inlined-imports mode: data.AgentFile is cleared; content is in data.ImportPaths. |
| 482 | // All current engines (Claude, Codex, Gemini, Copilot) use this mechanism: NativeAgentFile is false, |
| 483 | // and they read the fully-assembled prompt.txt in GetExecutionSteps. |
| 484 | |
| 485 | var userPromptChunks []string |
| 486 | var expressionMappings []*ExpressionMapping |
| 487 | |
| 488 | // Step 1a/1b: Process imports in declaration order, interleaving: |
| 489 | // - compile-time inlined markdown (imports with inputs) |
| 490 | // - runtime-import macros (imports without inputs) |
| 491 | // In older workflow data (without PromptImports), fall back to legacy grouped handling. |
| 492 | if len(data.PromptImports) > 0 { |
| 493 | compilerYamlLog.Printf("Processing %d ordered prompt import entries", len(data.PromptImports)) |
| 494 | workspaceRoot := "" |
| 495 | hasImportInputs := len(data.ImportInputs) > 0 |
| 496 | if data.InlinedImports && c.markdownPath != "" { |
| 497 | workspaceRoot = resolveWorkspaceRoot(c.markdownPath) |
| 498 | } |
| 499 | for _, entry := range data.PromptImports { |
| 500 | if entry.Markdown != "" { |
| 501 | cleaned := removeXMLComments(entry.Markdown) |
| 502 | if hasImportInputs { |
| 503 | cleaned = SubstituteImportInputs(cleaned, data.ImportInputs) |
| 504 | } |
| 505 | chunks, exprMaps := extractPromptChunksFromMarkdown(cleaned) |
| 506 | userPromptChunks = append(userPromptChunks, chunks...) |
| 507 | expressionMappings = append(expressionMappings, exprMaps...) |
| 508 | continue |
| 509 | } |
| 510 | if entry.ImportPath == "" { |
| 511 | continue |
| 512 | } |
| 513 | importPath := filepath.ToSlash(entry.ImportPath) |
| 514 | if workspaceRoot != "" { |
| 515 | rawContent, err := os.ReadFile(filepath.Join(workspaceRoot, importPath)) |
| 516 | if err != nil { |
| 517 | compilerYamlLog.Printf("Warning: failed to read import file %s (%v), falling back to runtime-import", importPath, err) |
| 518 | userPromptChunks = append(userPromptChunks, fmt.Sprintf("{{#runtime-import %s}}", importPath)) |
| 519 | continue |
| 520 | } |