executor.go 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595
  1. package workflow
  2. import (
  3. "archive/zip"
  4. "bytes"
  5. "context"
  6. "crypto/sha256"
  7. "encoding/hex"
  8. "encoding/json"
  9. "errors"
  10. "fmt"
  11. "io"
  12. "net/http"
  13. "path/filepath"
  14. "reflect"
  15. "strings"
  16. "sync"
  17. "sync/atomic"
  18. "time"
  19. )
  20. // executeServiceStep executes a Service_* step
  21. func (e *Engine) executeServiceStep(ctx ContextAccessor, step *Step) error {
  22. // Extract service name from step ID (Service_xxx)
  23. serviceName := step.ID[8:] // Remove "Service_" prefix
  24. // Evaluate input parameters (with deep evaluation for nested structures)
  25. evaluator := NewExpressionEvaluator(ctx)
  26. params := make(map[string]interface{})
  27. // Get base context for accessing fields
  28. baseCtx := ctx.GetBaseContext()
  29. for key, valueExpr := range step.In {
  30. val, err := evaluator.EvaluateDeep(valueExpr)
  31. if err != nil {
  32. return fmt.Errorf("failed to evaluate input parameter %s: %w", key, err)
  33. }
  34. params[key] = val
  35. }
  36. if baseCtx.ServiceAdapter == nil {
  37. return fmt.Errorf("service step %s: no ServiceAdapter configured", step.ID)
  38. }
  39. // Call service
  40. result, err := baseCtx.ServiceAdapter.Call(baseCtx.Ctx, serviceName, params)
  41. if err != nil {
  42. return fmt.Errorf("service call failed: %w", err)
  43. }
  44. // Store result in local variable (use ctx accessor for proper scoping in parallel mode)
  45. ctx.SetLocalVar("_result", result.Data)
  46. // Apply output mapping
  47. if len(step.Out) > 0 {
  48. if err := e.applyOutputMapping(ctx, step.Out); err != nil {
  49. return fmt.Errorf("failed to apply output mapping: %w", err)
  50. }
  51. }
  52. // Clear _result
  53. ctx.DeleteLocalVar("_result")
  54. return nil
  55. }
  56. // executeComponentStep executes a Component_* step
  57. func (e *Engine) executeComponentStep(ctx ContextAccessor, step *Step) error {
  58. // Extract component ID from step ID (Component_xxx)
  59. componentID := step.ID[10:] // Remove "Component_" prefix
  60. // Evaluate input parameters (with deep evaluation for nested structures)
  61. evaluator := NewExpressionEvaluator(ctx)
  62. params := make(map[string]interface{})
  63. // Get base context for accessing fields
  64. baseCtx := ctx.GetBaseContext()
  65. for key, valueExpr := range step.In {
  66. val, err := evaluator.EvaluateDeep(valueExpr)
  67. if err != nil {
  68. return fmt.Errorf("failed to evaluate input parameter %s: %w", key, err)
  69. }
  70. params[key] = val
  71. }
  72. if baseCtx.ComponentAdapter == nil {
  73. return fmt.Errorf("component step %s: no ComponentAdapter configured", step.ID)
  74. }
  75. // Call component
  76. result, err := baseCtx.ComponentAdapter.Call(baseCtx.Ctx, componentID, params)
  77. if err != nil {
  78. return fmt.Errorf("component call failed: %w", err)
  79. }
  80. // Store result in local variable (use ctx accessor for proper scoping in parallel mode)
  81. ctx.SetLocalVar("_result", result)
  82. // Apply output mapping
  83. if len(step.Out) > 0 {
  84. if err := e.applyOutputMapping(ctx, step.Out); err != nil {
  85. return fmt.Errorf("failed to apply output mapping: %w", err)
  86. }
  87. }
  88. // Clear _result
  89. ctx.DeleteLocalVar("_result")
  90. return nil
  91. }
  92. // resolveSchemaRef resolves schemaRef references in output_config.format (v3.9+)
  93. func (e *Engine) resolveSchemaRef(params map[string]interface{}) error {
  94. // Check if output_config exists
  95. outputConfig, ok := params["output_config"].(map[string]interface{})
  96. if !ok {
  97. return nil // No output_config, nothing to resolve
  98. }
  99. // Check if format exists
  100. format, ok := outputConfig["format"].(map[string]interface{})
  101. if !ok {
  102. return nil // No format, nothing to resolve
  103. }
  104. // Check if schemaRef exists
  105. schemaRef, hasSchemaRef := format["schemaRef"].(string)
  106. _, hasSchema := format["schema"]
  107. // If both schema and schemaRef exist, that's an error
  108. if hasSchema && hasSchemaRef {
  109. return fmt.Errorf("output_config.format cannot have both 'schema' and 'schemaRef' - use one or the other")
  110. }
  111. // If no schemaRef, nothing to resolve
  112. if !hasSchemaRef {
  113. return nil
  114. }
  115. // Resolve schemaRef from registry
  116. schema, err := e.workflow.Registry.GetSchema(schemaRef)
  117. if err != nil {
  118. return fmt.Errorf("failed to resolve schemaRef %q: %w", schemaRef, err)
  119. }
  120. // Replace schemaRef with the actual schema
  121. format["schema"] = schema
  122. delete(format, "schemaRef")
  123. return nil
  124. }
  125. // getOutputFormatType returns the output format type string from params.
  126. // Checks response_format.type (OpenAI style) then output_config.format.type (Anthropic style).
  127. // Returns "" when neither is set.
  128. func getOutputFormatType(params map[string]interface{}) string {
  129. if rf, ok := params["response_format"].(map[string]interface{}); ok {
  130. if t, ok := rf["type"].(string); ok {
  131. return t
  132. }
  133. }
  134. if oc, ok := params["output_config"].(map[string]interface{}); ok {
  135. if f, ok := oc["format"].(map[string]interface{}); ok {
  136. if t, ok := f["type"].(string); ok {
  137. return t
  138. }
  139. }
  140. }
  141. return ""
  142. }
  143. // isJSONObjectOutput checks if the params request unschema'd json_object output.
  144. // Supports both response_format (OpenAI) and output_config (Anthropic) styles.
  145. func isJSONObjectOutput(params map[string]interface{}) bool {
  146. return getOutputFormatType(params) == "json_object"
  147. }
  148. // stripMarkdownCodeFence removes a ```json or ``` opening fence and its closing
  149. // ``` from s, returning the inner text trimmed of whitespace.
  150. // Returns s unchanged when no fence is detected.
  151. func stripMarkdownCodeFence(s string) string {
  152. s = strings.TrimSpace(s)
  153. for _, prefix := range []string{"```json", "```"} {
  154. if strings.HasPrefix(s, prefix) {
  155. inner := strings.TrimPrefix(s, prefix)
  156. if idx := strings.LastIndex(inner, "```"); idx != -1 {
  157. return strings.TrimSpace(inner[:idx])
  158. }
  159. break
  160. }
  161. }
  162. return s
  163. }
  164. // unwrapJSONObject strips markdown fencing and JSON-parses the content when the
  165. // output format is json_object. Returns content unchanged for other formats or
  166. // when content is already a non-string value (adapter already parsed it).
  167. func unwrapJSONObject(content interface{}, params map[string]interface{}) interface{} {
  168. if !isJSONObjectOutput(params) {
  169. return content
  170. }
  171. s, ok := content.(string)
  172. if !ok {
  173. return content
  174. }
  175. s = stripMarkdownCodeFence(s)
  176. var parsed interface{}
  177. if err := json.Unmarshal([]byte(s), &parsed); err == nil {
  178. return parsed
  179. }
  180. return s // not valid JSON even after stripping — return stripped string
  181. }
  182. // isStructuredOutput checks if the params specify structured output (json_schema)
  183. // Supports both response_format (OpenAI) and output_config (Anthropic) styles
  184. func isStructuredOutput(params map[string]interface{}) bool {
  185. return getOutputFormatType(params) == "json_schema"
  186. }
  187. // executeLLMStep executes an LLM_* step
  188. func (e *Engine) executeLLMStep(ctx ContextAccessor, step *Step) error {
  189. // Get base context for accessing fields
  190. baseCtx := ctx.GetBaseContext()
  191. // Evaluate input parameters (with deep evaluation for nested structures)
  192. evaluator := NewExpressionEvaluator(ctx)
  193. params := make(map[string]interface{})
  194. for key, valueExpr := range step.In {
  195. val, err := evaluator.EvaluateDeep(valueExpr)
  196. if err != nil {
  197. return fmt.Errorf("failed to evaluate input parameter %s: %w", key, err)
  198. }
  199. params[key] = val
  200. }
  201. // Resolve docs and inject into system prompt
  202. if err := e.injectDocs(baseCtx, params); err != nil {
  203. return fmt.Errorf("failed to inject docs: %w", err)
  204. }
  205. // Resolve schemaRef in output_config (v3.9+)
  206. if err := e.resolveSchemaRef(params); err != nil {
  207. return fmt.Errorf("failed to resolve schemaRef: %w", err)
  208. }
  209. // Streaming is controlled by in.stream (passed in step.In / params).
  210. isStreaming := false
  211. if streamVal, ok := params["stream"].(bool); ok && streamVal {
  212. isStreaming = true
  213. }
  214. stream := make(chan string, 10)
  215. var streamDone chan struct{} // closed when the streaming goroutine finishes
  216. // The engine owns the stream lifecycle: it always closes stream after Call
  217. // returns. Adapters must NOT close stream themselves; they only send to it.
  218. var closeOnce sync.Once
  219. closeStream := func() { closeOnce.Do(func() { close(stream) }) }
  220. if isStreaming {
  221. streamDone = make(chan struct{})
  222. go func() {
  223. defer close(streamDone)
  224. stepID := step.ID
  225. for chunk := range stream {
  226. // Emit llm_token RunEvent (fire-and-forget, spec 3.12 §13.4)
  227. e.emitRunEvent(baseCtx, RunEventLLMToken, &stepID, map[string]interface{}{
  228. "delta": chunk,
  229. })
  230. }
  231. }()
  232. } else {
  233. closeStream() // Close immediately if not streaming
  234. }
  235. // v3.16+: Resolve per-node LLM adapter based on step.Model
  236. llmAdapter := baseCtx.LLMAdapter // default
  237. if step.Model != "" && baseCtx.LLMAdapterRegistry != nil {
  238. resolved, modelOverride, resolveErr := baseCtx.LLMAdapterRegistry.Resolve(step.Model)
  239. if resolveErr != nil {
  240. closeStream()
  241. return fmt.Errorf("LLM provider resolution failed for step %s: %w", step.ID, resolveErr)
  242. }
  243. llmAdapter = resolved
  244. if modelOverride != "" {
  245. params["model"] = modelOverride
  246. }
  247. }
  248. // Measure latency (adapter-agnostic timing)
  249. callStart := time.Now()
  250. // Call LLM
  251. result, err := llmAdapter.Call(baseCtx.Ctx, params, stream)
  252. latencyMs := time.Since(callStart).Milliseconds()
  253. // Always close stream after Call returns so the goroutine can finish,
  254. // even if the adapter did not close it. No-op if adapter already closed it.
  255. closeStream()
  256. // Wait for the streaming goroutine to drain all remaining chunks.
  257. if streamDone != nil {
  258. <-streamDone
  259. }
  260. if err != nil {
  261. if e.isV310OrLater() {
  262. // v3.10: Set partial _meta with whatever info we have
  263. ctx.SetLocalVar("_meta", buildMetaFromError(err, latencyMs))
  264. }
  265. return fmt.Errorf("LLM call failed: %w", err)
  266. }
  267. // Emit llm_done RunEvent (always on success, spec 3.12 §13.4)
  268. // Order: step_start → llm_token(×N, stream only) → llm_done → step_done
  269. {
  270. stepID := step.ID
  271. llmDonePayload := map[string]interface{}{
  272. "latency_ms": latencyMs,
  273. }
  274. if fr, ok := result["finish_reason"].(string); ok {
  275. llmDonePayload["finish_reason"] = fr
  276. }
  277. if m, ok := result["model"].(string); ok {
  278. llmDonePayload["model"] = m
  279. }
  280. if usage, ok := result["usage"].(map[string]interface{}); ok {
  281. normalized := map[string]interface{}{}
  282. if v, ok := usage["prompt_tokens"]; ok {
  283. normalized["input_tokens"] = v
  284. }
  285. if v, ok := usage["completion_tokens"]; ok {
  286. normalized["output_tokens"] = v
  287. }
  288. if v, ok := usage["total_tokens"]; ok {
  289. normalized["total_tokens"] = v
  290. }
  291. llmDonePayload["usage"] = normalized
  292. }
  293. e.emitRunEvent(baseCtx, RunEventLLMDone, &stepID, llmDonePayload)
  294. }
  295. // Version-dependent result handling
  296. if e.isV310OrLater() {
  297. // === v3.10 semantics: _result = content only, _meta = metadata ===
  298. e.applyV310LLMResult(ctx, result, params, latencyMs)
  299. } else {
  300. // === v3.6-v3.9 semantics (unchanged) ===
  301. if isStructuredOutput(params) {
  302. // For structured output, _result is the parsed content directly
  303. if content, ok := result["content"]; ok {
  304. ctx.SetLocalVar("_result", content)
  305. } else {
  306. ctx.SetLocalVar("_result", result)
  307. }
  308. } else {
  309. // For non-structured output, _result is the full result map (v3.6 behavior)
  310. ctx.SetLocalVar("_result", result)
  311. }
  312. }
  313. // Apply output mapping
  314. if len(step.Out) > 0 {
  315. if err := e.applyOutputMapping(ctx, step.Out); err != nil {
  316. return fmt.Errorf("failed to apply output mapping: %w", err)
  317. }
  318. }
  319. // Clear ephemeral local variables
  320. ctx.DeleteLocalVar("_result")
  321. if e.isV310OrLater() {
  322. ctx.DeleteLocalVar("_meta")
  323. }
  324. return nil
  325. }
  326. // applyV310LLMResult sets _result and _meta for v3.10 semantics
  327. func (e *Engine) applyV310LLMResult(ctx ContextAccessor, result map[string]interface{}, params map[string]interface{}, latencyMs int64) {
  328. // _result = content body only (string for text, parsed object for json_schema/json_object)
  329. if content, ok := result["content"]; ok {
  330. ctx.SetLocalVar("_result", unwrapJSONObject(content, params))
  331. } else {
  332. ctx.SetLocalVar("_result", nil)
  333. }
  334. // _meta = call metadata
  335. meta := map[string]interface{}{
  336. "latency_ms": latencyMs,
  337. }
  338. if model, ok := result["model"].(string); ok {
  339. meta["model"] = model
  340. meta["model_resolved"] = model
  341. meta["provider"] = inferProvider(model)
  342. }
  343. if finishReason, ok := result["finish_reason"].(string); ok {
  344. meta["finish_reason"] = finishReason
  345. }
  346. if responseID, ok := result["response_id"].(string); ok && responseID != "" {
  347. meta["response_id"] = responseID
  348. }
  349. // Build normalized usage
  350. if usage, ok := result["usage"].(map[string]interface{}); ok {
  351. normalizedUsage := map[string]interface{}{
  352. "raw": usage,
  353. }
  354. if pt, ok := usage["prompt_tokens"]; ok {
  355. normalizedUsage["input_tokens"] = pt
  356. }
  357. if ct, ok := usage["completion_tokens"]; ok {
  358. normalizedUsage["output_tokens"] = ct
  359. }
  360. if tt, ok := usage["total_tokens"]; ok {
  361. normalizedUsage["total_tokens"] = tt
  362. }
  363. meta["usage"] = normalizedUsage
  364. }
  365. ctx.SetLocalVar("_meta", meta)
  366. }
  367. // inferProvider infers the LLM provider from the model name
  368. func inferProvider(model string) string {
  369. m := strings.ToLower(model)
  370. switch {
  371. case strings.HasPrefix(m, "claude") || strings.Contains(m, "anthropic"):
  372. return "anthropic"
  373. case strings.HasPrefix(m, "gpt") || strings.HasPrefix(m, "o1") || strings.HasPrefix(m, "o3"):
  374. return "openai"
  375. case strings.Contains(m, "gemini"):
  376. return "google"
  377. case strings.Contains(m, "mistral"):
  378. return "mistral"
  379. default:
  380. return "unknown"
  381. }
  382. }
  383. // buildMetaFromError extracts partial metadata from an LLM error
  384. func buildMetaFromError(err error, latencyMs int64) map[string]interface{} {
  385. meta := map[string]interface{}{
  386. "latency_ms": latencyMs,
  387. }
  388. var llmErr *LLMError
  389. if errors.As(err, &llmErr) {
  390. if llmErr.Model != "" {
  391. meta["model"] = llmErr.Model
  392. meta["provider"] = inferProvider(llmErr.Model)
  393. }
  394. if llmErr.Provider != "" {
  395. meta["provider"] = llmErr.Provider
  396. }
  397. if llmErr.RequestID != "" {
  398. meta["request_id"] = llmErr.RequestID
  399. }
  400. }
  401. return meta
  402. }
  403. // injectDocs resolves doc IDs from the "docs" param and appends their content
  404. // to the last system message. If no system message exists, one is created.
  405. func (e *Engine) injectDocs(baseCtx *ExecutionContext, params map[string]interface{}) error {
  406. docIDs, ok := params["docs"].([]interface{})
  407. if !ok || len(docIDs) == 0 {
  408. return nil
  409. }
  410. if baseCtx.DocAdapter == nil {
  411. return fmt.Errorf("docs referenced but no DocAdapter configured")
  412. }
  413. // Resolve each doc ID
  414. var docContents []string
  415. for _, raw := range docIDs {
  416. docID := fmt.Sprintf("%v", raw)
  417. // Validate doc is declared in registry
  418. if !e.workflow.Registry.HasDoc(docID) {
  419. return fmt.Errorf("doc %q not declared in registry.docs", docID)
  420. }
  421. content, err := baseCtx.DocAdapter.Get(baseCtx.Ctx, docID)
  422. if err != nil {
  423. return fmt.Errorf("failed to resolve doc %s: %w", docID, err)
  424. }
  425. desc := e.workflow.Registry.Docs[docID]
  426. docContents = append(docContents, fmt.Sprintf("[Doc %s: %s]\n%s", docID, desc, content))
  427. }
  428. if len(docContents) == 0 {
  429. return nil
  430. }
  431. docBlock := strings.Join(docContents, "\n\n")
  432. // Find the last system message and append doc content
  433. messages, ok := params["messages"].([]interface{})
  434. if !ok {
  435. return nil
  436. }
  437. lastSystemIdx := -1
  438. for i, m := range messages {
  439. if msg, ok := m.(map[string]interface{}); ok {
  440. if role, _ := msg["role"].(string); role == "system" {
  441. lastSystemIdx = i
  442. }
  443. }
  444. }
  445. if lastSystemIdx >= 0 {
  446. msg := messages[lastSystemIdx].(map[string]interface{})
  447. existing, _ := msg["content"].(string)
  448. msg["content"] = existing + "\n\n" + docBlock
  449. } else {
  450. // No system message — prepend one
  451. sysMsg := map[string]interface{}{
  452. "role": "system",
  453. "content": docBlock,
  454. }
  455. params["messages"] = append([]interface{}{sysMsg}, messages...)
  456. }
  457. // Remove docs from params so it's not passed to the LLM adapter
  458. delete(params, "docs")
  459. return nil
  460. }
  461. // executeAPIStep executes an API_* step
  462. func (e *Engine) executeAPIStep(ctx ContextAccessor, step *Step) error {
  463. // Extract API ID from step ID (API_xxx)
  464. apiID := step.ID[4:] // Remove "API_" prefix
  465. // Get base context for accessing fields
  466. baseCtx := ctx.GetBaseContext()
  467. // Get API definition from registry
  468. apiDef, err := e.workflow.Registry.GetAPIDefinition(apiID)
  469. if err != nil {
  470. return fmt.Errorf("API definition not found: %w", err)
  471. }
  472. // Evaluate input parameters (with deep evaluation for nested structures)
  473. evaluator := NewExpressionEvaluator(ctx)
  474. params := make(map[string]interface{})
  475. for key, valueExpr := range step.In {
  476. val, err := evaluator.EvaluateDeep(valueExpr)
  477. if err != nil {
  478. return fmt.Errorf("failed to evaluate input parameter %s: %w", key, err)
  479. }
  480. params[key] = val
  481. }
  482. // Resolve auth token if specified
  483. if apiDef.Auth != "" {
  484. var authToken interface{}
  485. var err error
  486. if strings.HasPrefix(apiDef.Auth, "=") {
  487. authToken, err = evaluator.EvaluateValue(apiDef.Auth)
  488. } else {
  489. authToken, err = evaluator.Evaluate(apiDef.Auth)
  490. }
  491. if err != nil {
  492. return fmt.Errorf("failed to resolve auth token: %w", err)
  493. }
  494. if authToken != nil {
  495. if tokenStr, ok := authToken.(string); ok {
  496. params["authToken"] = tokenStr
  497. }
  498. }
  499. }
  500. if baseCtx.APIAdapter == nil {
  501. return fmt.Errorf("API step %s: no APIAdapter configured", step.ID)
  502. }
  503. // Call API
  504. result, err := baseCtx.APIAdapter.Call(baseCtx.Ctx, apiDef, params)
  505. if err != nil {
  506. return fmt.Errorf("API call failed: %w", err)
  507. }
  508. // Store result in local variable (use ctx accessor for proper scoping in parallel mode)
  509. ctx.SetLocalVar("_result", result)
  510. // Apply output mapping
  511. if len(step.Out) > 0 {
  512. if err := e.applyOutputMapping(ctx, step.Out); err != nil {
  513. return fmt.Errorf("failed to apply output mapping: %w", err)
  514. }
  515. }
  516. // Clear _result
  517. ctx.DeleteLocalVar("_result")
  518. return nil
  519. }
  520. // executeSetStep executes a Set_* step
  521. func (e *Engine) executeSetStep(ctx ContextAccessor, step *Step) error {
  522. evaluator := NewExpressionEvaluator(ctx)
  523. // Evaluate value expression
  524. value, err := evaluator.EvaluateValue(step.Value)
  525. if err != nil {
  526. return fmt.Errorf("failed to evaluate value: %w", err)
  527. }
  528. // Set variable
  529. if err := evaluator.SetVariable(step.Target, value); err != nil {
  530. return fmt.Errorf("failed to set variable: %w", err)
  531. }
  532. return nil
  533. }
  534. // executeWriteStep executes a Write_* step
  535. func (e *Engine) executeWriteStep(ctx ContextAccessor, step *Step) error {
  536. evaluator := NewExpressionEvaluator(ctx)
  537. // Get base context for accessing fields
  538. baseCtx := ctx.GetBaseContext()
  539. // Evaluate target path
  540. targetPath, err := evaluator.EvaluateValue(step.Target)
  541. if err != nil {
  542. return fmt.Errorf("failed to evaluate target path: %w", err)
  543. }
  544. targetPathStr, ok := targetPath.(string)
  545. if !ok {
  546. return fmt.Errorf("target path must be a string, got: %T", targetPath)
  547. }
  548. // Resolve {var} interpolations (e.g. ".tmp/{_iterDir}/item.txt")
  549. targetPathStr, err = e.interpolateFilePath(ctx, targetPathStr)
  550. if err != nil {
  551. return fmt.Errorf("failed to interpolate target path: %w", err)
  552. }
  553. // Resolve .tmp/ prefix to run-isolated path before artifact check
  554. targetPathStr = e.resolveTmpPath(ctx, targetPathStr)
  555. // Check if path is allowed (check against logical/resolved path)
  556. if !e.workflow.Registry.IsArtifactPathAllowed(targetPathStr) {
  557. return fmt.Errorf("write path not allowed: %s", targetPathStr)
  558. }
  559. // Evaluate value
  560. value, err := evaluator.EvaluateValue(step.Value)
  561. if err != nil {
  562. return fmt.Errorf("failed to evaluate value: %w", err)
  563. }
  564. // Convert value to bytes
  565. var content []byte
  566. switch v := value.(type) {
  567. case string:
  568. content = []byte(v)
  569. case []byte:
  570. content = v
  571. default:
  572. // Serialize non-string values as JSON
  573. jsonBytes, err := json.Marshal(v)
  574. if err != nil {
  575. return fmt.Errorf("failed to marshal value to JSON: %w", err)
  576. }
  577. content = jsonBytes
  578. }
  579. // Determine write mode
  580. mode := WriteModeOverwrite
  581. if step.Mode != "" {
  582. mode = WriteMode(step.Mode)
  583. }
  584. if baseCtx.FileAdapter == nil {
  585. return fmt.Errorf("write step %s: no FileAdapter configured", step.ID)
  586. }
  587. // Write file
  588. if err := baseCtx.FileAdapter.Write(baseCtx.Ctx, targetPathStr, content, mode); err != nil {
  589. return fmt.Errorf("failed to write file: %w", err)
  590. }
  591. // Store artifact reference
  592. baseCtx.Artifacts[targetPathStr] = targetPathStr
  593. // Emit file_done RunEvent (spec 3.13 §13.3)
  594. {
  595. stepID := step.ID
  596. e.emitRunEvent(baseCtx, RunEventFileDone, &stepID, map[string]interface{}{
  597. "path": strings.TrimPrefix(targetPathStr, "/"),
  598. "size_bytes": len(content),
  599. })
  600. }
  601. return nil
  602. }
  603. // executeBranchStep executes a Branch_* step
  604. func (e *Engine) executeBranchStep(ctx ContextAccessor, step *Step) error {
  605. evaluator := NewExpressionEvaluator(ctx)
  606. // Evaluate cases to find the first matching one
  607. var selectedStepID string
  608. for _, c := range step.Cases {
  609. if len(c) != 2 {
  610. return fmt.Errorf("invalid case format: expected [expression, stepId], got %d elements", len(c))
  611. }
  612. expression := c[0]
  613. stepID := c[1]
  614. if expression == "ELSE" {
  615. selectedStepID = stepID
  616. break
  617. }
  618. var result interface{}
  619. var err error
  620. if strings.HasPrefix(expression, "=") {
  621. result, err = evaluator.EvaluateValue(expression)
  622. } else {
  623. result, err = evaluator.Evaluate(expression)
  624. }
  625. if err != nil {
  626. return fmt.Errorf("failed to evaluate branch condition: %w", err)
  627. }
  628. if toBool(result) {
  629. selectedStepID = stepID
  630. break
  631. }
  632. }
  633. if selectedStepID == "" {
  634. // No matching case and no ELSE: skip all branch bodies, proceed to Branch_*.next.
  635. // Spec §10.10: "若无命中且无 ELSE,跳过所有分支子链,继续 Branch_*.next"
  636. return nil
  637. }
  638. // Execute the selected branch
  639. branchStep := e.findStepByID(selectedStepID)
  640. if branchStep == nil {
  641. return fmt.Errorf("branch step not found: %s", selectedStepID)
  642. }
  643. return e.executeStep(ctx, branchStep)
  644. }
  645. // executeLoopStep executes a Loop_* step, dispatching to while or source mode.
  646. func (e *Engine) executeLoopStep(ctx ContextAccessor, step *Step) error {
  647. // v3.16+: while mode (condition-based loop)
  648. if step.While != "" {
  649. return e.executeWhileLoop(ctx, step)
  650. }
  651. return e.executeSourceLoop(ctx, step)
  652. }
  653. // executeSourceLoop executes a Loop_* step with a source array.
  654. func (e *Engine) executeSourceLoop(ctx ContextAccessor, step *Step) error {
  655. evaluator := NewExpressionEvaluator(ctx)
  656. // Get loop source from step.Source property.
  657. sourceStr, ok := step.Source.(string)
  658. if !ok || sourceStr == "" {
  659. return fmt.Errorf("loop source not specified or not a string")
  660. }
  661. // Evaluate source using new expression convention (supports = prefix)
  662. // For backward compatibility: if no = prefix, treat as direct variable reference
  663. sourceExpr := sourceStr
  664. if !strings.HasPrefix(sourceExpr, "=") {
  665. // Add = prefix for backward compatibility with old format ($items)
  666. sourceExpr = "=" + sourceExpr
  667. }
  668. source, err := evaluator.EvaluateValue(sourceExpr)
  669. if err != nil {
  670. return fmt.Errorf("failed to evaluate loop source: %w", err)
  671. }
  672. // Convert source to slice
  673. sourceSlice := reflect.ValueOf(source)
  674. if sourceSlice.Kind() != reflect.Slice && sourceSlice.Kind() != reflect.Array {
  675. return fmt.Errorf("loop source must be an array, got: %T", source)
  676. }
  677. // Get loop mode
  678. mode := LoopMode(step.Mode)
  679. if mode != LoopModeParallel && mode != LoopModeSerial {
  680. return fmt.Errorf("invalid loop mode: %s", step.Mode)
  681. }
  682. // Execute loop iterations
  683. length := sourceSlice.Len()
  684. // v3.16+: cap at min(len(source), maxIterations) if maxIterations is set
  685. if step.MaxIterations != nil && *step.MaxIterations < length {
  686. length = *step.MaxIterations
  687. }
  688. if mode == LoopModeSerial {
  689. // Serial execution
  690. for i := 0; i < length; i++ {
  691. item := sourceSlice.Index(i).Interface()
  692. if err := e.executeLoopIteration(ctx, step, item, i); err != nil {
  693. // v3.16+: BREAK exits the loop cleanly (not an error)
  694. if IsBreakError(err) {
  695. return nil
  696. }
  697. return fmt.Errorf("loop iteration %d failed: %w", i, err)
  698. }
  699. }
  700. } else {
  701. // Parallel execution with proper synchronization
  702. if err := e.executeLoopParallel(ctx, step, sourceSlice, length); err != nil {
  703. return err
  704. }
  705. }
  706. return nil
  707. }
  708. // executeWhileLoop executes a Loop_* step with a while condition (v3.16+).
  709. // While mode is always serial. _item is not available; _index and _iterDir are.
  710. func (e *Engine) executeWhileLoop(ctx ContextAccessor, step *Step) error {
  711. maxIter := *step.MaxIterations // guaranteed non-nil by validation
  712. for i := 0; i < maxIter; i++ {
  713. // Evaluate while condition BEFORE each iteration (including iteration 0)
  714. evaluator := NewExpressionEvaluator(ctx)
  715. whileExpr := step.While
  716. var condResult interface{}
  717. var err error
  718. if strings.HasPrefix(whileExpr, "=") {
  719. condResult, err = evaluator.EvaluateValue(whileExpr)
  720. } else {
  721. condResult, err = evaluator.Evaluate(whileExpr)
  722. }
  723. if err != nil {
  724. return fmt.Errorf("failed to evaluate while condition: %w", err)
  725. }
  726. if !toBool(condResult) {
  727. break // Condition false, exit loop
  728. }
  729. // Set loop local variables: _index and _iterDir (no _item for while loops)
  730. ctx.SetLocalVar("_index", i)
  731. ctx.SetLocalVar("_iterDir", fmt.Sprintf("%s_%d", step.ID, i))
  732. // Execute children
  733. var childErr error
  734. for _, childID := range step.Children {
  735. child := e.findStepByID(childID)
  736. if child == nil {
  737. childErr = fmt.Errorf("loop child step not found: %s", childID)
  738. break
  739. }
  740. if err := e.executeStep(ctx, child); err != nil {
  741. childErr = err
  742. break
  743. }
  744. }
  745. // Cleanup locals
  746. ctx.DeleteLocalVar("_index")
  747. ctx.DeleteLocalVar("_iterDir")
  748. if childErr != nil {
  749. // v3.16+: BREAK exits the loop cleanly
  750. if IsBreakError(childErr) {
  751. return nil
  752. }
  753. return fmt.Errorf("while loop iteration %d failed: %w", i, childErr)
  754. }
  755. }
  756. return nil
  757. }
  758. // executeLoopParallel executes loop iterations in parallel
  759. func (e *Engine) executeLoopParallel(ctx ContextAccessor, loopStep *Step, sourceSlice reflect.Value, length int) error {
  760. // Get base context for accessing fields
  761. baseCtx := ctx.GetBaseContext()
  762. // Wrap execution context with thread-safe accessor
  763. safeCtx := NewSafeExecutionContext(baseCtx)
  764. // Get parallel executor
  765. executor := e.getParallelExecutor()
  766. // v3.16+: For BREAK support in parallel, use a shared atomic flag.
  767. // When any iteration triggers BREAK, the flag is set. Subsequent iterations
  768. // check the flag before starting and skip. Already-running iterations complete normally.
  769. var breakRequested int32 // atomic: 0=no, 1=yes
  770. // Create branches for each iteration
  771. branches := make([]ParallelBranch, length)
  772. for i := 0; i < length; i++ {
  773. item := sourceSlice.Index(i).Interface()
  774. index := i
  775. branches[i] = ParallelBranch{
  776. ID: fmt.Sprintf("%s[%d]", loopStep.ID, index),
  777. Fn: func(branchCtx context.Context) error {
  778. // v3.16+: Check if BREAK was already requested by another iteration
  779. if atomic.LoadInt32(&breakRequested) != 0 {
  780. return nil // Skip this iteration
  781. }
  782. // Create child context with isolated local vars
  783. childCtx := NewChildExecutionContext(safeCtx)
  784. childCtx.SetLocalVar("_item", item)
  785. childCtx.SetLocalVar("_index", index)
  786. // v3.14+: inject _iterDir for per-iteration .tmp/ isolation
  787. childCtx.SetLocalVar("_iterDir", fmt.Sprintf("%s_%d", loopStep.ID, index))
  788. // Execute children in child context
  789. for _, childID := range loopStep.Children {
  790. // Check cancellation before each child
  791. select {
  792. case <-branchCtx.Done():
  793. return branchCtx.Err()
  794. default:
  795. }
  796. child := e.findStepByID(childID)
  797. if child == nil {
  798. return fmt.Errorf("loop child step not found: %s", childID)
  799. }
  800. // Execute with child context - works because executeStep accepts ContextAccessor
  801. if err := e.executeStep(childCtx, child); err != nil {
  802. // v3.16+: BREAK signals loop exit — set flag and return cleanly
  803. if IsBreakError(err) {
  804. atomic.StoreInt32(&breakRequested, 1)
  805. return nil // This iteration ends, others complete naturally
  806. }
  807. return fmt.Errorf("iteration %d: %w", index, err)
  808. }
  809. }
  810. return nil
  811. },
  812. }
  813. }
  814. // Execute all branches in parallel
  815. if err := executor.Execute(baseCtx.Ctx, branches, e.getErrorStrategy()); err != nil {
  816. return fmt.Errorf("parallel loop execution failed: %w", err)
  817. }
  818. return nil
  819. }
  820. // executeLoopIteration executes a single loop iteration
  821. func (e *Engine) executeLoopIteration(ctx ContextAccessor, loopStep *Step, item interface{}, index int) error {
  822. // Set loop local variables (use ctx accessor for proper scoping)
  823. ctx.SetLocalVar("_item", item)
  824. ctx.SetLocalVar("_index", index)
  825. // v3.14+: inject _iterDir for per-iteration .tmp/ isolation
  826. ctx.SetLocalVar("_iterDir", fmt.Sprintf("%s_%d", loopStep.ID, index))
  827. defer ctx.DeleteLocalVar("_item")
  828. defer ctx.DeleteLocalVar("_index")
  829. defer ctx.DeleteLocalVar("_iterDir")
  830. // Execute children
  831. for _, childID := range loopStep.Children {
  832. child := e.findStepByID(childID)
  833. if child == nil {
  834. return fmt.Errorf("loop child step not found: %s", childID)
  835. }
  836. if err := e.executeStep(ctx, child); err != nil {
  837. return err
  838. }
  839. }
  840. return nil
  841. }
  842. // applyOutputMapping applies the output mapping from _result to global variables and files
  843. // Keys starting with $ write to variables, keys starting with / write to files.
  844. func (e *Engine) applyOutputMapping(ctx ContextAccessor, outMapping StepOutput) error {
  845. evaluator := NewExpressionEvaluator(ctx)
  846. // Get base context for file adapter and RunEvent emission
  847. baseCtx := ctx.GetBaseContext()
  848. for target, valueExpr := range outMapping {
  849. // Evaluate the value expression
  850. value, err := evaluator.EvaluateValue(valueExpr)
  851. if err != nil {
  852. return fmt.Errorf("failed to evaluate output expression for %s: %w", target, err)
  853. }
  854. // Determine if this is a file write or variable write
  855. if strings.HasPrefix(target, "/") {
  856. // File write - interpolate path variables
  857. filePath, err := e.interpolateFilePath(ctx, target)
  858. if err != nil {
  859. return fmt.Errorf("failed to interpolate file path %s: %w", target, err)
  860. }
  861. // Convert value to bytes
  862. var content []byte
  863. switch v := value.(type) {
  864. case string:
  865. content = []byte(v)
  866. case []byte:
  867. content = v
  868. default:
  869. // Serialize non-string values as JSON
  870. jsonBytes, err := json.Marshal(v)
  871. if err != nil {
  872. return fmt.Errorf("failed to marshal value to JSON: %w", err)
  873. }
  874. content = jsonBytes
  875. }
  876. if baseCtx.FileAdapter == nil {
  877. return fmt.Errorf("output mapping: no FileAdapter configured for file target %s", target)
  878. }
  879. // Write file
  880. if err := baseCtx.FileAdapter.Write(baseCtx.Ctx, filePath, content, WriteModeOverwrite); err != nil {
  881. return fmt.Errorf("failed to write output file %s: %w", filePath, err)
  882. }
  883. // Store artifact reference
  884. baseCtx.Artifacts[filePath] = filePath
  885. // Emit file_done RunEvent (spec 3.13 §13.3)
  886. // Order: llm_done → file_done(×N) → step_done
  887. {
  888. stepID := baseCtx.CurrentStepID
  889. e.emitRunEvent(baseCtx, RunEventFileDone, &stepID, map[string]interface{}{
  890. "path": strings.TrimPrefix(filePath, "/"),
  891. "size_bytes": len(content),
  892. })
  893. }
  894. } else if strings.HasPrefix(target, "$") {
  895. // Variable write - may include path with _index interpolation
  896. resolvedTarget, err := e.interpolateVarPath(ctx, target)
  897. if err != nil {
  898. return fmt.Errorf("failed to interpolate var path %s: %w", target, err)
  899. }
  900. // Set the variable
  901. if err := evaluator.SetVariable(resolvedTarget, value); err != nil {
  902. return fmt.Errorf("failed to set output variable %s: %w", resolvedTarget, err)
  903. }
  904. } else {
  905. return fmt.Errorf("invalid output target %s: must start with $ (variable) or / (file)", target)
  906. }
  907. }
  908. return nil
  909. }
  910. // interpolateFilePath interpolates variables in file paths using {var} syntax
  911. // Example: "/vsc/{_item.path}" with _item.path = "files/fileA.ts" -> "/vsc/files/fileA.ts"
  912. // Each {expr} is evaluated exactly once; substituted values are not re-scanned.
  913. func (e *Engine) interpolateFilePath(ctx ContextAccessor, path string) (string, error) {
  914. evaluator := NewExpressionEvaluator(ctx)
  915. var sb strings.Builder
  916. remaining := path
  917. for {
  918. start := strings.Index(remaining, "{")
  919. if start == -1 {
  920. sb.WriteString(remaining)
  921. break
  922. }
  923. end := strings.Index(remaining[start:], "}")
  924. if end == -1 {
  925. sb.WriteString(remaining)
  926. break
  927. }
  928. end += start
  929. // Write the literal prefix before the expression
  930. sb.WriteString(remaining[:start])
  931. // Evaluate the expression inside { }
  932. expr := remaining[start+1 : end]
  933. val, err := evaluator.Evaluate(expr)
  934. if err != nil {
  935. return "", fmt.Errorf("failed to evaluate path expression %s: %w", expr, err)
  936. }
  937. sb.WriteString(fmt.Sprintf("%v", val))
  938. // Advance past the closing brace; substituted value is not re-scanned
  939. remaining = remaining[end+1:]
  940. }
  941. return sb.String(), nil
  942. }
  943. // interpolateVarPath interpolates _index in variable paths
  944. // Example: "$generated[_index].name" with _index = 2 -> "$generated[2].name"
  945. func (e *Engine) interpolateVarPath(ctx ContextAccessor, path string) (string, error) {
  946. // Check if path contains _index
  947. if !strings.Contains(path, "_index") {
  948. return path, nil
  949. }
  950. // Get _index value
  951. indexVal, ok := ctx.GetLocalVar("_index")
  952. if !ok {
  953. return path, nil // No _index, return as-is
  954. }
  955. // Replace _index with actual value
  956. return strings.ReplaceAll(path, "_index", fmt.Sprintf("%v", indexVal)), nil
  957. }
  958. // resolveTmpPath rewrites .tmp/xxx paths to .tmp/{runID}/xxx for run-level isolation (v3.14+).
  959. // Paths that do not start with ".tmp/" are returned unchanged.
  960. func (e *Engine) resolveTmpPath(ctx ContextAccessor, path string) string {
  961. if !strings.HasPrefix(path, ".tmp/") {
  962. return path
  963. }
  964. baseCtx := ctx.GetBaseContext()
  965. runID := baseCtx.WorkflowID
  966. rest := strings.TrimPrefix(path, ".tmp/")
  967. return ".tmp/" + runID + "/" + rest
  968. }
  969. // resolveStepFilePath evaluates a file path expression and applies {var} interpolation
  970. // and .tmp/ run-isolation in one step. Used by Download_* and Unzip_*.
  971. func (e *Engine) resolveStepFilePath(ctx ContextAccessor, raw string) (string, error) {
  972. evaluator := NewExpressionEvaluator(ctx)
  973. // Step 1: handle = expressions ("=$var", "=\"literal\"", etc.)
  974. val, err := evaluator.EvaluateValue(raw)
  975. if err != nil {
  976. return "", fmt.Errorf("failed to evaluate path expression: %w", err)
  977. }
  978. pathStr, ok := val.(string)
  979. if !ok {
  980. return "", fmt.Errorf("path expression must yield a string, got %T", val)
  981. }
  982. // Step 2: resolve {var} interpolations (e.g. ".tmp/{_iterDir}/bundle.zip")
  983. pathStr, err = e.interpolateFilePath(ctx, pathStr)
  984. if err != nil {
  985. return "", fmt.Errorf("failed to interpolate path: %w", err)
  986. }
  987. // Step 3: rewrite .tmp/ prefix for run isolation
  988. return e.resolveTmpPath(ctx, pathStr), nil
  989. }
  990. // routePathByExt maps a filename to a target directory using the routeByExt table.
  991. // It returns defaultDir when no extension matches. An empty defaultDir is allowed
  992. // (caller should treat it as "discard / skip" if needed).
  993. func routePathByExt(name string, routeByExt map[string]string, defaultDir string) string {
  994. ext := strings.ToLower(filepath.Ext(name))
  995. if dir, ok := routeByExt[ext]; ok {
  996. return dir
  997. }
  998. return defaultDir
  999. }
  1000. // zipSlipSafe returns true if the entry path is safe (no path traversal).
  1001. func zipSlipSafe(entryName string) bool {
  1002. if strings.HasPrefix(entryName, "/") {
  1003. return false
  1004. }
  1005. // filepath.Clean normalises ".." components; if the result escapes the root it's unsafe
  1006. cleaned := filepath.Clean(entryName)
  1007. if strings.HasPrefix(cleaned, "..") {
  1008. return false
  1009. }
  1010. // Reject absolute paths on any OS (Windows drive letters: "C:\")
  1011. if filepath.IsAbs(cleaned) {
  1012. return false
  1013. }
  1014. return true
  1015. }
  1016. // executeDownloadStep executes a Download_* step (v3.14+).
  1017. // It downloads a single file from an external URL and writes it to the artifact space,
  1018. // either to an explicit target path or to a directory chosen by routeByExt.
  1019. func (e *Engine) executeDownloadStep(ctx ContextAccessor, step *Step) error {
  1020. baseCtx := ctx.GetBaseContext()
  1021. evaluator := NewExpressionEvaluator(ctx)
  1022. if baseCtx.FileAdapter == nil {
  1023. return fmt.Errorf("download step %s: no FileAdapter configured", step.ID)
  1024. }
  1025. // --- Resolve source ---
  1026. var downloadURL string
  1027. var extraHeaders map[string]string
  1028. sourceVal, err := evaluator.EvaluateValue(step.Source)
  1029. if err != nil {
  1030. return fmt.Errorf("download step %s: failed to evaluate source: %w", step.ID, err)
  1031. }
  1032. switch sv := sourceVal.(type) {
  1033. case string:
  1034. downloadURL = sv
  1035. case map[string]interface{}:
  1036. // Object form: {url, headers, auth, timeout, checksum}
  1037. urlVal, ok := sv["url"]
  1038. if !ok {
  1039. return fmt.Errorf("download step %s: source object missing 'url' field", step.ID)
  1040. }
  1041. downloadURL, ok = urlVal.(string)
  1042. if !ok {
  1043. return fmt.Errorf("download step %s: source.url must be a string", step.ID)
  1044. }
  1045. if hdrs, ok := sv["headers"]; ok {
  1046. if hdrMap, ok := hdrs.(map[string]interface{}); ok {
  1047. extraHeaders = make(map[string]string, len(hdrMap))
  1048. for k, v := range hdrMap {
  1049. extraHeaders[k] = fmt.Sprintf("%v", v)
  1050. }
  1051. }
  1052. }
  1053. default:
  1054. return fmt.Errorf("download step %s: source must be a URL string or object, got %T", step.ID, sourceVal)
  1055. }
  1056. if downloadURL == "" {
  1057. return fmt.Errorf("download step %s: download URL is empty", step.ID)
  1058. }
  1059. // --- HTTP download (streaming into memory buffer) ---
  1060. httpCtx, cancel := context.WithTimeout(baseCtx.Ctx, 5*time.Minute)
  1061. defer cancel()
  1062. req, err := http.NewRequestWithContext(httpCtx, http.MethodGet, downloadURL, nil)
  1063. if err != nil {
  1064. return fmt.Errorf("download step %s: failed to build HTTP request: %w", step.ID, err)
  1065. }
  1066. for k, v := range extraHeaders {
  1067. req.Header.Set(k, v)
  1068. }
  1069. resp, err := http.DefaultClient.Do(req)
  1070. if err != nil {
  1071. return fmt.Errorf("download step %s: HTTP request failed: %w", step.ID, err)
  1072. }
  1073. defer resp.Body.Close()
  1074. if resp.StatusCode < 200 || resp.StatusCode >= 300 {
  1075. return fmt.Errorf("download step %s: server returned HTTP %d", step.ID, resp.StatusCode)
  1076. }
  1077. content, err := io.ReadAll(resp.Body)
  1078. if err != nil {
  1079. return fmt.Errorf("download step %s: failed to read response body: %w", step.ID, err)
  1080. }
  1081. // --- Determine target path(s) ---
  1082. // writtenPath captures the resolved artifact path for _result output.
  1083. var writtenPath string
  1084. writeFile := func(targetPath string, data []byte) error {
  1085. // Resolve {var} interpolation and .tmp/ isolation
  1086. resolved, err := e.resolveStepFilePath(ctx, targetPath)
  1087. if err != nil {
  1088. return err
  1089. }
  1090. if !e.workflow.Registry.IsArtifactPathAllowed(resolved) {
  1091. return fmt.Errorf("download target path not allowed: %s", resolved)
  1092. }
  1093. if err := baseCtx.FileAdapter.Write(baseCtx.Ctx, resolved, data, WriteModeOverwrite); err != nil {
  1094. return fmt.Errorf("failed to write download to %s: %w", resolved, err)
  1095. }
  1096. writtenPath = resolved
  1097. baseCtx.Artifacts[resolved] = resolved
  1098. stepID := step.ID
  1099. e.emitRunEvent(baseCtx, RunEventFileDone, &stepID, map[string]interface{}{
  1100. "path": resolved,
  1101. "size_bytes": len(data),
  1102. })
  1103. return nil
  1104. }
  1105. var writeErr error
  1106. if step.Target != "" {
  1107. // Single explicit target
  1108. writeErr = writeFile(step.Target, content)
  1109. } else if len(step.RouteByExt) > 0 || step.DefaultDir != "" {
  1110. // Route by extension: derive filename from URL
  1111. urlPath := downloadURL
  1112. if idx := strings.Index(urlPath, "?"); idx >= 0 {
  1113. urlPath = urlPath[:idx]
  1114. }
  1115. filename := filepath.Base(urlPath)
  1116. if filename == "" || filename == "." {
  1117. filename = "download"
  1118. }
  1119. dir := routePathByExt(filename, step.RouteByExt, step.DefaultDir)
  1120. if dir != "" {
  1121. targetPath := strings.TrimSuffix(dir, "/") + "/" + filename
  1122. writeErr = writeFile(targetPath, content)
  1123. }
  1124. // dir == "": no matching extension and no defaultDir → skip (not an error)
  1125. } else {
  1126. return fmt.Errorf("download step %s: must specify either 'target' or 'routeByExt'", step.ID)
  1127. }
  1128. if writeErr != nil {
  1129. return writeErr
  1130. }
  1131. // Set _result and apply out mapping (same pattern as Service_*/Component_* steps)
  1132. ctx.SetLocalVar("_result", map[string]interface{}{
  1133. "path": writtenPath,
  1134. })
  1135. if len(step.Out) > 0 {
  1136. if err := e.applyOutputMapping(ctx, step.Out); err != nil {
  1137. ctx.DeleteLocalVar("_result")
  1138. return fmt.Errorf("download step %s: failed to apply output mapping: %w", step.ID, err)
  1139. }
  1140. }
  1141. ctx.DeleteLocalVar("_result")
  1142. return nil
  1143. }
  1144. // executeUnzipStep executes an Unzip_* step (v3.14+).
  1145. // It reads a zip archive from the artifact space, extracts each entry, and writes
  1146. // the contents to directories determined by routeByExt (mandatory) / defaultDir (optional).
  1147. // zip-slip entries (path traversal) are rejected.
  1148. func (e *Engine) executeUnzipStep(ctx ContextAccessor, step *Step) error {
  1149. baseCtx := ctx.GetBaseContext()
  1150. evaluator := NewExpressionEvaluator(ctx)
  1151. if baseCtx.FileAdapter == nil {
  1152. return fmt.Errorf("unzip step %s: no FileAdapter configured", step.ID)
  1153. }
  1154. // --- Resolve source path ---
  1155. sourceVal, err := evaluator.EvaluateValue(step.Source)
  1156. if err != nil {
  1157. return fmt.Errorf("unzip step %s: failed to evaluate source: %w", step.ID, err)
  1158. }
  1159. sourceStr, ok := sourceVal.(string)
  1160. if !ok {
  1161. return fmt.Errorf("unzip step %s: source must be a string path expression, got %T", step.ID, sourceVal)
  1162. }
  1163. sourcePath, err := e.resolveStepFilePath(ctx, sourceStr)
  1164. if err != nil {
  1165. return fmt.Errorf("unzip step %s: failed to resolve source path: %w", step.ID, err)
  1166. }
  1167. // --- Read the zip file ---
  1168. zipBytes, err := baseCtx.FileAdapter.Read(baseCtx.Ctx, sourcePath)
  1169. if err != nil {
  1170. return fmt.Errorf("unzip step %s: failed to read zip file %s: %w", step.ID, sourcePath, err)
  1171. }
  1172. // --- Open zip reader ---
  1173. zr, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes)))
  1174. if err != nil {
  1175. return fmt.Errorf("unzip step %s: failed to open zip archive: %w", step.ID, err)
  1176. }
  1177. // --- Determine overwrite mode ---
  1178. overwrite := true
  1179. if step.Overwrite != nil {
  1180. overwrite = *step.Overwrite
  1181. }
  1182. writeMode := WriteModeOverwrite
  1183. if !overwrite {
  1184. writeMode = WriteModeFailIfExists
  1185. }
  1186. // --- Extract entries ---
  1187. var extractedPaths []string
  1188. for _, entry := range zr.File {
  1189. if entry.FileInfo().IsDir() {
  1190. continue // directories are created implicitly
  1191. }
  1192. entryName := entry.Name
  1193. // zip-slip protection
  1194. if !zipSlipSafe(entryName) {
  1195. return fmt.Errorf("unzip step %s: unsafe entry path rejected: %s", step.ID, entryName)
  1196. }
  1197. // Route by extension
  1198. dir := routePathByExt(entryName, step.RouteByExt, step.DefaultDir)
  1199. if dir == "" {
  1200. continue // no matching rule and no defaultDir: skip
  1201. }
  1202. baseName := filepath.Base(entryName)
  1203. targetPath := strings.TrimSuffix(dir, "/") + "/" + baseName
  1204. // Resolve .tmp/ isolation
  1205. resolvedPath := e.resolveTmpPath(ctx, targetPath)
  1206. if !e.workflow.Registry.IsArtifactPathAllowed(resolvedPath) {
  1207. return fmt.Errorf("unzip step %s: target path not allowed: %s", step.ID, resolvedPath)
  1208. }
  1209. // Read entry content
  1210. rc, err := entry.Open()
  1211. if err != nil {
  1212. return fmt.Errorf("unzip step %s: failed to open entry %s: %w", step.ID, entryName, err)
  1213. }
  1214. entryBytes, err := io.ReadAll(rc)
  1215. rc.Close()
  1216. if err != nil {
  1217. return fmt.Errorf("unzip step %s: failed to read entry %s: %w", step.ID, entryName, err)
  1218. }
  1219. // Write to FileAdapter
  1220. if err := baseCtx.FileAdapter.Write(baseCtx.Ctx, resolvedPath, entryBytes, writeMode); err != nil {
  1221. return fmt.Errorf("unzip step %s: failed to write %s: %w", step.ID, resolvedPath, err)
  1222. }
  1223. baseCtx.Artifacts[resolvedPath] = resolvedPath
  1224. stepID := step.ID
  1225. e.emitRunEvent(baseCtx, RunEventFileDone, &stepID, map[string]interface{}{
  1226. "path": resolvedPath,
  1227. "size_bytes": len(entryBytes),
  1228. })
  1229. extractedPaths = append(extractedPaths, resolvedPath)
  1230. }
  1231. // Set _result to extraction summary, then apply out mapping (same pattern as other steps)
  1232. ctx.SetLocalVar("_result", map[string]interface{}{
  1233. "count": len(extractedPaths),
  1234. "files": extractedPaths,
  1235. })
  1236. if len(step.Out) > 0 {
  1237. if err := e.applyOutputMapping(ctx, step.Out); err != nil {
  1238. ctx.DeleteLocalVar("_result")
  1239. return fmt.Errorf("unzip step %s: failed to apply output mapping: %w", step.ID, err)
  1240. }
  1241. }
  1242. ctx.DeleteLocalVar("_result")
  1243. return nil
  1244. }
  1245. // executePauseStep implements the Pause_* node (v3.15+).
  1246. // It blocks the workflow goroutine until the workflow is resumed via Engine.Resume,
  1247. // times out (if timeout is configured), or the context is cancelled.
  1248. //
  1249. // Unlike other step executors, executePauseStep is responsible for emitting step_done
  1250. // and routing to step.Next after a successful resume (similar to how Stop_* handles its
  1251. // own terminal routing). The caller (executeStep) returns early on nil to skip the
  1252. // standard post-switch step_done / next logic.
  1253. func (e *Engine) executePauseStep(ctx ContextAccessor, step *Step, stepStartTime time.Time, stepTypePattern string) error {
  1254. baseCtx := ctx.GetBaseContext()
  1255. stepID := step.ID
  1256. // Generate a unique wait token for this pause instance.
  1257. // The token is stored and published as a SHA-256 hex digest so that the
  1258. // persisted "waitToken (hash)" matches the spec §11.3.1 requirement.
  1259. // Callers obtain the token from the pause_start RunEvent and must supply
  1260. // the same value in ResumeRequest.Token.
  1261. rawToken := fmt.Sprintf("wt_%s_%s_%d", baseCtx.WorkflowID, step.ID, time.Now().UnixNano())
  1262. h := sha256.Sum256([]byte(rawToken))
  1263. token := hex.EncodeToString(h[:])
  1264. // Initialise PauseState and publish it so Resume() can find it.
  1265. state := &PauseState{
  1266. ch: make(chan resumeSignal, 1),
  1267. token: token,
  1268. nodeID: step.ID,
  1269. seenRequestIDs: make(map[string]bool),
  1270. }
  1271. baseCtx.pauseMu.Lock()
  1272. baseCtx.PauseState = state
  1273. baseCtx.pauseMu.Unlock()
  1274. // Calculate optional expiry timestamp for the pause_start payload.
  1275. var expireAtStr string
  1276. if step.Timeout != nil {
  1277. expireAt := time.Now().Add(time.Duration(step.Timeout.Sec) * time.Second)
  1278. expireAtStr = expireAt.UTC().Format("2006-01-02T15:04:05.000Z07:00")
  1279. }
  1280. // Set workflow status to paused.
  1281. ctx.SetStatus(StatusPaused)
  1282. // Emit pause_start.
  1283. pausePayload := map[string]interface{}{
  1284. "nodeId": stepID,
  1285. "waitToken": token,
  1286. "resumeResultTarget": step.ResumeResultTarget,
  1287. }
  1288. if step.Reason != "" {
  1289. pausePayload["reason"] = step.Reason
  1290. }
  1291. if expireAtStr != "" {
  1292. pausePayload["expireAt"] = expireAtStr
  1293. }
  1294. e.emitRunEvent(baseCtx, RunEventPauseStart, &stepID, pausePayload)
  1295. // Block waiting for resume signal, timeout, or context cancellation.
  1296. var sig resumeSignal
  1297. var timedOut bool
  1298. if step.Timeout != nil {
  1299. timer := time.NewTimer(time.Duration(step.Timeout.Sec) * time.Second)
  1300. defer timer.Stop()
  1301. select {
  1302. case sig = <-state.ch:
  1303. // Resume signal received.
  1304. case <-timer.C:
  1305. timedOut = true
  1306. case <-baseCtx.Ctx.Done():
  1307. ctx.SetStatus(StatusFailed)
  1308. return baseCtx.Ctx.Err()
  1309. }
  1310. } else {
  1311. select {
  1312. case sig = <-state.ch:
  1313. // Resume signal received.
  1314. case <-baseCtx.Ctx.Done():
  1315. ctx.SetStatus(StatusFailed)
  1316. return baseCtx.Ctx.Err()
  1317. }
  1318. }
  1319. if timedOut {
  1320. // Emit pause_timeout.
  1321. e.emitRunEvent(baseCtx, RunEventPauseTimeout, &stepID, map[string]interface{}{
  1322. "nodeId": stepID,
  1323. "expiredAt": time.Now().UTC().Format("2006-01-02T15:04:05.000Z07:00"),
  1324. "timeoutAction": step.Timeout.On,
  1325. })
  1326. // Resume execution at the timeout handler.
  1327. // Status is set back to running; the timeout step (typically Stop_* or an
  1328. // error-handler chain) is responsible for further status transitions.
  1329. ctx.SetStatus(StatusRunning)
  1330. timeoutStep := e.findStepByID(step.Timeout.On)
  1331. if timeoutStep == nil {
  1332. return fmt.Errorf("Pause_* step %s: timeout.on step %q not found", step.ID, step.Timeout.On)
  1333. }
  1334. return e.executeStep(ctx, timeoutStep)
  1335. }
  1336. // --- Resume path ---
  1337. // Write the resume payload to resumeResultTarget in $vars.
  1338. evaluator := NewExpressionEvaluator(ctx)
  1339. if err := evaluator.SetVariable(step.ResumeResultTarget, sig.Payload); err != nil {
  1340. return fmt.Errorf("Pause_* step %s: failed to write resume payload to %q: %w", step.ID, step.ResumeResultTarget, err)
  1341. }
  1342. // Emit pause_resumed.
  1343. e.emitRunEvent(baseCtx, RunEventPauseResumed, &stepID, map[string]interface{}{
  1344. "nodeId": stepID,
  1345. "requestId": sig.RequestID,
  1346. "resumedAt": time.Now().UTC().Format("2006-01-02T15:04:05.000Z07:00"),
  1347. })
  1348. // Restore running status.
  1349. ctx.SetStatus(StatusRunning)
  1350. // Note: Pause_* does NOT support the `print` field (spec §5.1: 不适用 for Pause_*).
  1351. // step_print is intentionally omitted here.
  1352. // Emit step_done for the Pause_* node (successful resume path only).
  1353. e.emitRunEvent(baseCtx, RunEventStepDone, &stepID, map[string]interface{}{
  1354. "step_type": stepTypePattern,
  1355. "duration_ms": time.Since(stepStartTime).Milliseconds(),
  1356. })
  1357. // Continue to the next step.
  1358. if step.Next == "" {
  1359. return fmt.Errorf("Pause_* step %s is missing required 'next' field", step.ID)
  1360. }
  1361. if step.Next == "RETURN" {
  1362. return nil
  1363. }
  1364. nextStep := e.findStepByID(step.Next)
  1365. if nextStep == nil {
  1366. return fmt.Errorf("next step not found: %s (referenced by Pause_* step %s)", step.Next, step.ID)
  1367. }
  1368. return e.executeStep(ctx, nextStep)
  1369. }