@studio-foundation/engine 0.3.0-beta.1 → 0.3.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/dist/__tests__/engine.conditions-group.test.d.ts +2 -0
  2. package/dist/__tests__/engine.conditions-group.test.d.ts.map +1 -0
  3. package/dist/__tests__/engine.conditions-group.test.js +222 -0
  4. package/dist/__tests__/engine.conditions-group.test.js.map +1 -0
  5. package/dist/__tests__/engine.conditions.test.d.ts +2 -0
  6. package/dist/__tests__/engine.conditions.test.d.ts.map +1 -0
  7. package/dist/__tests__/engine.conditions.test.js +164 -0
  8. package/dist/__tests__/engine.conditions.test.js.map +1 -0
  9. package/dist/__tests__/engine.resume.test.d.ts +2 -0
  10. package/dist/__tests__/engine.resume.test.d.ts.map +1 -0
  11. package/dist/__tests__/engine.resume.test.js +93 -0
  12. package/dist/__tests__/engine.resume.test.js.map +1 -0
  13. package/dist/__tests__/engine.script-stage.test.d.ts +2 -0
  14. package/dist/__tests__/engine.script-stage.test.d.ts.map +1 -0
  15. package/dist/__tests__/engine.script-stage.test.js +108 -0
  16. package/dist/__tests__/engine.script-stage.test.js.map +1 -0
  17. package/dist/db/client.d.ts +3 -0
  18. package/dist/db/client.d.ts.map +1 -0
  19. package/{src/db/client.ts → dist/db/client.js} +1 -2
  20. package/dist/db/client.js.map +1 -0
  21. package/dist/engine.context-event.test.d.ts +2 -0
  22. package/dist/engine.context-event.test.d.ts.map +1 -0
  23. package/dist/engine.context-event.test.js +111 -0
  24. package/dist/engine.context-event.test.js.map +1 -0
  25. package/dist/engine.d.ts +52 -0
  26. package/dist/engine.d.ts.map +1 -0
  27. package/dist/engine.js +364 -0
  28. package/dist/engine.js.map +1 -0
  29. package/dist/events.d.ts +163 -0
  30. package/dist/events.d.ts.map +1 -0
  31. package/dist/events.js +12 -0
  32. package/dist/events.js.map +1 -0
  33. package/{src/index.ts → dist/index.d.ts} +3 -45
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +24 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/pipeline/agent-loader.d.ts +4 -0
  38. package/dist/pipeline/agent-loader.d.ts.map +1 -0
  39. package/dist/pipeline/agent-loader.js +28 -0
  40. package/dist/pipeline/agent-loader.js.map +1 -0
  41. package/dist/pipeline/agent-loader.test.d.ts +2 -0
  42. package/dist/pipeline/agent-loader.test.d.ts.map +1 -0
  43. package/dist/pipeline/agent-loader.test.js +134 -0
  44. package/dist/pipeline/agent-loader.test.js.map +1 -0
  45. package/dist/pipeline/condition-evaluator.d.ts +6 -0
  46. package/dist/pipeline/condition-evaluator.d.ts.map +1 -0
  47. package/dist/pipeline/condition-evaluator.js +105 -0
  48. package/dist/pipeline/condition-evaluator.js.map +1 -0
  49. package/dist/pipeline/condition-evaluator.test.d.ts +2 -0
  50. package/dist/pipeline/condition-evaluator.test.d.ts.map +1 -0
  51. package/dist/pipeline/condition-evaluator.test.js +103 -0
  52. package/dist/pipeline/condition-evaluator.test.js.map +1 -0
  53. package/dist/pipeline/context-pack-loader.d.ts +3 -0
  54. package/dist/pipeline/context-pack-loader.d.ts.map +1 -0
  55. package/dist/pipeline/context-pack-loader.js +51 -0
  56. package/dist/pipeline/context-pack-loader.js.map +1 -0
  57. package/dist/pipeline/context-propagation.d.ts +28 -0
  58. package/dist/pipeline/context-propagation.d.ts.map +1 -0
  59. package/dist/pipeline/context-propagation.js +161 -0
  60. package/dist/pipeline/context-propagation.js.map +1 -0
  61. package/dist/pipeline/context-propagation.test.d.ts +2 -0
  62. package/dist/pipeline/context-propagation.test.d.ts.map +1 -0
  63. package/dist/pipeline/context-propagation.test.js +194 -0
  64. package/dist/pipeline/context-propagation.test.js.map +1 -0
  65. package/dist/pipeline/contract-loader.d.ts +4 -0
  66. package/dist/pipeline/contract-loader.d.ts.map +1 -0
  67. package/dist/pipeline/contract-loader.js +30 -0
  68. package/dist/pipeline/contract-loader.js.map +1 -0
  69. package/dist/pipeline/group-orchestrator.d.ts +19 -0
  70. package/dist/pipeline/group-orchestrator.d.ts.map +1 -0
  71. package/dist/pipeline/group-orchestrator.js +376 -0
  72. package/dist/pipeline/group-orchestrator.js.map +1 -0
  73. package/dist/pipeline/hook-executor.d.ts +28 -0
  74. package/dist/pipeline/hook-executor.d.ts.map +1 -0
  75. package/dist/pipeline/hook-executor.js +63 -0
  76. package/dist/pipeline/hook-executor.js.map +1 -0
  77. package/dist/pipeline/hook-executor.test.d.ts +2 -0
  78. package/dist/pipeline/hook-executor.test.d.ts.map +1 -0
  79. package/dist/pipeline/hook-executor.test.js +71 -0
  80. package/dist/pipeline/hook-executor.test.js.map +1 -0
  81. package/dist/pipeline/invariants-loader.d.ts +6 -0
  82. package/dist/pipeline/invariants-loader.d.ts.map +1 -0
  83. package/dist/pipeline/invariants-loader.js +15 -0
  84. package/dist/pipeline/invariants-loader.js.map +1 -0
  85. package/dist/pipeline/invariants-loader.test.d.ts +2 -0
  86. package/dist/pipeline/invariants-loader.test.d.ts.map +1 -0
  87. package/dist/pipeline/invariants-loader.test.js +39 -0
  88. package/dist/pipeline/invariants-loader.test.js.map +1 -0
  89. package/dist/pipeline/loader.d.ts +5 -0
  90. package/dist/pipeline/loader.d.ts.map +1 -0
  91. package/dist/pipeline/loader.js +132 -0
  92. package/dist/pipeline/loader.js.map +1 -0
  93. package/dist/pipeline/loader.test.d.ts +2 -0
  94. package/dist/pipeline/loader.test.d.ts.map +1 -0
  95. package/dist/pipeline/loader.test.js +120 -0
  96. package/dist/pipeline/loader.test.js.map +1 -0
  97. package/dist/pipeline/output-validator.d.ts +10 -0
  98. package/dist/pipeline/output-validator.d.ts.map +1 -0
  99. package/dist/pipeline/output-validator.js +20 -0
  100. package/dist/pipeline/output-validator.js.map +1 -0
  101. package/dist/pipeline/output-validator.test.d.ts +2 -0
  102. package/dist/pipeline/output-validator.test.d.ts.map +1 -0
  103. package/dist/pipeline/output-validator.test.js +77 -0
  104. package/dist/pipeline/output-validator.test.js.map +1 -0
  105. package/dist/pipeline/post-validator.d.ts +8 -0
  106. package/dist/pipeline/post-validator.d.ts.map +1 -0
  107. package/dist/pipeline/post-validator.js +103 -0
  108. package/dist/pipeline/post-validator.js.map +1 -0
  109. package/dist/pipeline/skill-loader.d.ts +10 -0
  110. package/dist/pipeline/skill-loader.d.ts.map +1 -0
  111. package/dist/pipeline/skill-loader.js +26 -0
  112. package/dist/pipeline/skill-loader.js.map +1 -0
  113. package/dist/pipeline/skill-loader.test.d.ts +2 -0
  114. package/dist/pipeline/skill-loader.test.d.ts.map +1 -0
  115. package/dist/pipeline/skill-loader.test.js +38 -0
  116. package/dist/pipeline/skill-loader.test.js.map +1 -0
  117. package/dist/pipeline/stage-executor.d.ts +26 -0
  118. package/dist/pipeline/stage-executor.d.ts.map +1 -0
  119. package/dist/pipeline/stage-executor.js +540 -0
  120. package/dist/pipeline/stage-executor.js.map +1 -0
  121. package/dist/pipeline/stage-resolver.d.ts +3 -0
  122. package/dist/pipeline/stage-resolver.d.ts.map +1 -0
  123. package/dist/pipeline/stage-resolver.js +6 -0
  124. package/dist/pipeline/stage-resolver.js.map +1 -0
  125. package/dist/pipeline/startup-executor.d.ts +3 -0
  126. package/dist/pipeline/startup-executor.d.ts.map +1 -0
  127. package/dist/pipeline/startup-executor.js +23 -0
  128. package/dist/pipeline/startup-executor.js.map +1 -0
  129. package/dist/pipeline/startup-executor.test.d.ts +2 -0
  130. package/dist/pipeline/startup-executor.test.d.ts.map +1 -0
  131. package/dist/pipeline/startup-executor.test.js +34 -0
  132. package/dist/pipeline/startup-executor.test.js.map +1 -0
  133. package/dist/pipeline/types.d.ts +28 -0
  134. package/dist/pipeline/types.d.ts.map +1 -0
  135. package/dist/pipeline/types.js +12 -0
  136. package/dist/pipeline/types.js.map +1 -0
  137. package/dist/repo-resolver.d.ts +10 -0
  138. package/dist/repo-resolver.d.ts.map +1 -0
  139. package/dist/repo-resolver.js +39 -0
  140. package/dist/repo-resolver.js.map +1 -0
  141. package/dist/spawners/direct-engine-spawner.d.ts +8 -0
  142. package/dist/spawners/direct-engine-spawner.d.ts.map +1 -0
  143. package/dist/spawners/direct-engine-spawner.js +24 -0
  144. package/dist/spawners/direct-engine-spawner.js.map +1 -0
  145. package/dist/state/run-store.d.ts +74 -0
  146. package/dist/state/run-store.d.ts.map +1 -0
  147. package/dist/state/run-store.js +269 -0
  148. package/dist/state/run-store.js.map +1 -0
  149. package/dist/state/run-store.test.d.ts +2 -0
  150. package/dist/state/run-store.test.d.ts.map +1 -0
  151. package/dist/state/run-store.test.js +130 -0
  152. package/dist/state/run-store.test.js.map +1 -0
  153. package/dist/state/state-machine.d.ts +6 -0
  154. package/dist/state/state-machine.d.ts.map +1 -0
  155. package/dist/state/state-machine.js +27 -0
  156. package/dist/state/state-machine.js.map +1 -0
  157. package/dist/state/status-derivation.d.ts +12 -0
  158. package/dist/state/status-derivation.d.ts.map +1 -0
  159. package/dist/state/status-derivation.js +30 -0
  160. package/dist/state/status-derivation.js.map +1 -0
  161. package/package.json +7 -4
  162. package/ARCHITECTURE.md +0 -57
  163. package/src/__tests__/__fixtures__/script-stage/contracts/book-context.contract.yaml +0 -4
  164. package/src/__tests__/engine.conditions-group.test.ts +0 -244
  165. package/src/__tests__/engine.conditions.test.ts +0 -186
  166. package/src/__tests__/engine.resume.test.ts +0 -108
  167. package/src/__tests__/engine.script-stage.test.ts +0 -125
  168. package/src/engine.context-event.test.ts +0 -175
  169. package/src/engine.ts +0 -491
  170. package/src/events.ts +0 -167
  171. package/src/pipeline/agent-loader.test.ts +0 -151
  172. package/src/pipeline/agent-loader.ts +0 -39
  173. package/src/pipeline/condition-evaluator.test.ts +0 -129
  174. package/src/pipeline/condition-evaluator.ts +0 -121
  175. package/src/pipeline/context-pack-loader.ts +0 -63
  176. package/src/pipeline/context-propagation.test.ts +0 -237
  177. package/src/pipeline/context-propagation.ts +0 -235
  178. package/src/pipeline/contract-loader.ts +0 -41
  179. package/src/pipeline/group-orchestrator.ts +0 -483
  180. package/src/pipeline/hook-executor.test.ts +0 -121
  181. package/src/pipeline/hook-executor.ts +0 -87
  182. package/src/pipeline/invariants-loader.test.ts +0 -51
  183. package/src/pipeline/invariants-loader.ts +0 -14
  184. package/src/pipeline/loader.test.ts +0 -128
  185. package/src/pipeline/loader.ts +0 -149
  186. package/src/pipeline/output-validator.test.ts +0 -100
  187. package/src/pipeline/output-validator.ts +0 -40
  188. package/src/pipeline/post-validator.ts +0 -124
  189. package/src/pipeline/skill-loader.test.ts +0 -44
  190. package/src/pipeline/skill-loader.ts +0 -32
  191. package/src/pipeline/stage-executor.ts +0 -654
  192. package/src/pipeline/stage-resolver.ts +0 -8
  193. package/src/pipeline/startup-executor.test.ts +0 -37
  194. package/src/pipeline/startup-executor.ts +0 -32
  195. package/src/pipeline/types.ts +0 -42
  196. package/src/repo-resolver.ts +0 -61
  197. package/src/spawners/direct-engine-spawner.ts +0 -26
  198. package/src/state/run-store.test.ts +0 -157
  199. package/src/state/run-store.ts +0 -362
  200. package/src/state/state-machine.ts +0 -35
  201. package/src/state/status-derivation.ts +0 -36
  202. package/tests/context-pack-loader.test.ts +0 -113
  203. package/tests/context-propagation.test.ts +0 -267
  204. package/tests/direct-engine-spawner.test.ts +0 -102
  205. package/tests/e2e/feature-v5.test.ts +0 -57
  206. package/tests/events.test.ts +0 -56
  207. package/tests/fixtures/agents/test-agent.agent.yaml +0 -5
  208. package/tests/fixtures/contracts/code-gen.contract.yaml +0 -6
  209. package/tests/fixtures/contracts/qa-gate.contract.yaml +0 -15
  210. package/tests/fixtures/contracts/test-contract.contract.yaml +0 -6
  211. package/tests/fixtures/pipelines/group-test.pipeline.yaml +0 -40
  212. package/tests/fixtures/pipelines/simple.pipeline.yaml +0 -15
  213. package/tests/fixtures/pipelines/two-stage.pipeline.yaml +0 -24
  214. package/tests/fixtures/software/pipelines/feature-builder.pipeline.yaml +0 -75
  215. package/tests/fixtures/test-project/agents/test-agent.agent.yaml +0 -5
  216. package/tests/fixtures/test-project/contracts/basic-result.contract.yaml +0 -6
  217. package/tests/fixtures/test-project/contracts/code-gen.contract.yaml +0 -6
  218. package/tests/fixtures/test-project/contracts/maximum-only.contract.yaml +0 -8
  219. package/tests/fixtures/test-project/contracts/qa-gate.contract.yaml +0 -15
  220. package/tests/fixtures/test-project/contracts/strict-result.contract.yaml +0 -7
  221. package/tests/fixtures/test-project/contracts/test-contract.contract.yaml +0 -6
  222. package/tests/fixtures/test-project/pipelines/group-simple.pipeline.yaml +0 -26
  223. package/tests/fixtures/test-project/pipelines/group-test.pipeline.yaml +0 -40
  224. package/tests/fixtures/test-project/pipelines/hook-output-template.pipeline.yaml +0 -19
  225. package/tests/fixtures/test-project/pipelines/hook-reject-on-failure.pipeline.yaml +0 -19
  226. package/tests/fixtures/test-project/pipelines/maximum-only.pipeline.yaml +0 -15
  227. package/tests/fixtures/test-project/pipelines/parallel-collect-all-test.pipeline.yaml +0 -39
  228. package/tests/fixtures/test-project/pipelines/parallel-context-isolation-test.pipeline.yaml +0 -38
  229. package/tests/fixtures/test-project/pipelines/parallel-fail-test.pipeline.yaml +0 -39
  230. package/tests/fixtures/test-project/pipelines/parallel-test.pipeline.yaml +0 -39
  231. package/tests/fixtures/test-project/pipelines/parallel-then-sequential-test.pipeline.yaml +0 -38
  232. package/tests/fixtures/test-project/pipelines/simple.pipeline.yaml +0 -15
  233. package/tests/fixtures/test-project/pipelines/two-stage.pipeline.yaml +0 -24
  234. package/tests/fixtures/test-project/pipelines/with-startup.pipeline.yaml +0 -19
  235. package/tests/loader.test.ts +0 -385
  236. package/tests/post-validator.test.ts +0 -297
  237. package/tests/repo-resolver.test.ts +0 -102
  238. package/tests/run-store.test.ts +0 -143
  239. package/tests/state-machine.test.ts +0 -110
  240. package/tests/unit/context-propagation.test.ts +0 -45
  241. package/tests/unit/engine.test.ts +0 -770
  242. package/tests/unit/group-loop.test.ts +0 -389
  243. package/tests/unit/group-parallel.test.ts +0 -423
  244. package/tests/unit/state/status-derivation.test.ts +0 -88
  245. package/tsconfig.json +0 -24
  246. package/vitest.config.ts +0 -14
package/src/engine.ts DELETED
@@ -1,491 +0,0 @@
1
- // Main pipeline engine — the orchestrator
2
- // Connects ralph (retry loop) + runner (agent execution) + contracts (validation)
3
-
4
- import { randomUUID } from 'node:crypto';
5
- import type {
6
- PipelineEntry,
7
- PipelineRun,
8
- PipelineDefinition,
9
- RunSpawner,
10
- StageRun,
11
- ToolCall,
12
- } from '@studio-foundation/contracts';
13
- import { isStageGroup } from '@studio-foundation/contracts';
14
- import {
15
- type ToolRegistry,
16
- type ProviderRegistry,
17
- AnonymizationMiddleware,
18
- createStudioRunTool,
19
- STUDIO_RUN_PROMPT_SNIPPET,
20
- } from '@studio-foundation/runner';
21
- import { loadPipelineByName } from './pipeline/loader.js';
22
- import { executeStartupCommands } from './pipeline/startup-executor.js';
23
- import { loadInvariantsFile } from './pipeline/invariants-loader.js';
24
- import {
25
- createInitialContext,
26
- addStageOutput,
27
- addStageToolResults,
28
- clearGroupFeedback,
29
- } from './pipeline/context-propagation.js';
30
- import type { AnyRunStore } from './state/run-store.js';
31
- import type { EngineEvents } from './events.js';
32
- import { PipelineEventEmitter } from './events.js';
33
- import { resolveProjectPaths } from './pipeline/types.js';
34
- import { StageExecutor } from './pipeline/stage-executor.js';
35
- import { GroupOrchestrator } from './pipeline/group-orchestrator.js';
36
-
37
- export interface EngineConfig {
38
- configsDir: string;
39
- repoPath?: string;
40
- providerRegistry: ProviderRegistry;
41
- toolRegistry?: ToolRegistry; // optional — not needed for script-only pipelines
42
- db?: AnyRunStore;
43
- providerOverride?: string;
44
- /**
45
- * Skills content from active plugins, keyed by plugin name.
46
- * Each entry is an array of formatted markdown strings to inject
47
- * into the system prompt of agents that declare the plugin.
48
- */
49
- pluginSkills?: Record<string, string[]>;
50
- spawner?: RunSpawner; // if set, studio-run tool is available to agents
51
- maxDepth?: number; // max nesting depth for spawned runs, default 3
52
- defaultProvider?: string; // fallback when agent YAML omits provider
53
- defaultModel?: string; // fallback when agent YAML omits model
54
- }
55
-
56
- export interface RunInput {
57
- id?: string; // ← pre-generated run ID (e.g. from the API)
58
- pipeline?: string; // pipeline name (loads from YAML file)
59
- pipelineDef?: PipelineDefinition; // inline pipeline definition (skips YAML loading — for tests/programmatic use)
60
- input?: string | Record<string, unknown>;
61
- userInput?: string | Record<string, unknown>; // alias for input (used in tests and programmatic API)
62
- meta?: Record<string, unknown>;
63
- anonymize?: boolean;
64
- signal?: AbortSignal;
65
- depth?: number; // nesting depth (0 = top-level)
66
- parentRunId?: string; // parent run ID if spawned by another pipeline
67
- // Resume fields — used by `studio replay --restart`
68
- resumeFromStage?: string;
69
- priorStageOutputs?: Map<string, unknown>;
70
- priorStageToolResults?: Map<string, ToolCall[]>;
71
- originalRunId?: string;
72
- }
73
-
74
- /**
75
- * Collect all leaf stage names in pipeline order (group containers are transparent).
76
- */
77
- function collectLeafStageNames(entries: PipelineEntry[]): string[] {
78
- const names: string[] = [];
79
- for (const entry of entries) {
80
- if (isStageGroup(entry)) {
81
- for (const stage of entry.stages) names.push(stage.name);
82
- } else {
83
- names.push(entry.name);
84
- }
85
- }
86
- return names;
87
- }
88
-
89
- /**
90
- * Returns the set of stage names that should be skipped (all leaf stages before resumeFromStage).
91
- * Throws if resumeFromStage is not found in the pipeline.
92
- */
93
- function buildSkipSet(entries: PipelineEntry[], resumeFromStage: string): Set<string> {
94
- const leafNames = collectLeafStageNames(entries);
95
- const targetIndex = leafNames.indexOf(resumeFromStage);
96
- if (targetIndex < 0) {
97
- throw new Error(`Stage "${resumeFromStage}" not found in pipeline`);
98
- }
99
- return new Set(leafNames.slice(0, targetIndex));
100
- }
101
-
102
- function countTotalStages(entries: PipelineEntry[]): number {
103
- let count = 0;
104
- for (const entry of entries) {
105
- if (isStageGroup(entry)) {
106
- count += entry.stages.length;
107
- } else {
108
- count++;
109
- }
110
- }
111
- return count;
112
- }
113
-
114
- /**
115
- * Create a StageRun for a skipped stage (used during resume operations).
116
- * Uses randomUUID() to ensure unique IDs across multiple resume runs.
117
- */
118
- function makeSkippedStageRun(stageName: string, reason: string): StageRun {
119
- const now = new Date().toISOString();
120
- return {
121
- id: randomUUID(),
122
- stage_name: stageName,
123
- status: 'skipped',
124
- started_at: now,
125
- completed_at: now,
126
- tasks: [],
127
- skipped_reason: reason,
128
- };
129
- }
130
-
131
- export class PipelineEngine {
132
- private emitter: PipelineEventEmitter;
133
- private pipelineTotals = { tokens: 0, toolCalls: 0 };
134
- private stageExecutor: StageExecutor;
135
- private groupOrchestrator: GroupOrchestrator;
136
-
137
- constructor(
138
- private config: EngineConfig,
139
- private events?: EngineEvents,
140
- ) {
141
- this.emitter = new PipelineEventEmitter();
142
- this.stageExecutor = new StageExecutor({
143
- events,
144
- emitter: this.emitter,
145
- providerRegistry: config.providerRegistry,
146
- repoPath: config.repoPath,
147
- configsDir: config.configsDir,
148
- pluginSkills: config.pluginSkills,
149
- providerOverride: config.providerOverride,
150
- defaultProvider: config.defaultProvider,
151
- defaultModel: config.defaultModel,
152
- });
153
- this.groupOrchestrator = new GroupOrchestrator({
154
- events,
155
- emitter: this.emitter,
156
- stageExecutor: this.stageExecutor,
157
- });
158
- }
159
-
160
- async run(input: RunInput): Promise<PipelineRun> {
161
- const signal = input.signal;
162
-
163
- // Resolve the effective user input (support both 'input' and 'userInput' aliases)
164
- const userInputValue: string | Record<string, unknown> = input.userInput ?? input.input ?? '';
165
-
166
- if (!input.pipeline && !input.pipelineDef) {
167
- throw new Error('RunInput must provide either "pipeline" (pipeline name) or "pipelineDef" (inline definition)');
168
- }
169
-
170
- // 1. Resolve paths — configsDir is now the project root directly
171
- const projectPaths = resolveProjectPaths(this.config.configsDir);
172
-
173
- // 2. Load the pipeline — either from an inline definition or a YAML file
174
- const pipeline: PipelineDefinition = input.pipelineDef
175
- ? input.pipelineDef
176
- : await loadPipelineByName(input.pipeline!, projectPaths.pipelinesDir);
177
-
178
- // 2. Create the PipelineRun
179
- const pipelineRun: PipelineRun = {
180
- id: input.id ?? randomUUID(),
181
- pipeline_name: pipeline.name,
182
- status: 'running',
183
- started_at: new Date().toISOString(),
184
- stages: [],
185
- ...(typeof userInputValue === 'object' && userInputValue !== null
186
- ? { input: userInputValue as Record<string, unknown> }
187
- : {}),
188
- ...(input.parentRunId ? { parent_run_id: input.parentRunId } : {}),
189
- };
190
-
191
- // Reset totals for this run
192
- this.pipelineTotals = { tokens: 0, toolCalls: 0 };
193
- const pipelineStartTime = Date.now();
194
-
195
- // Create anonymization middleware for this run if requested via RunInput flag
196
- const runAnonymize = input.anonymize === true;
197
- const runMiddleware = runAnonymize ? new AnonymizationMiddleware() : null;
198
-
199
- // Persist the run immediately so log_path can be written before terminal states
200
- await this.config.db?.savePipelineRun(pipelineRun);
201
-
202
- // Build per-run tool registry: clone the shared registry and inject studio-run
203
- // with run-specific context (run ID, depth) if a spawner is configured.
204
- // For script-only pipelines, toolRegistry may be undefined.
205
- const runToolRegistry = this.config.spawner && this.config.toolRegistry
206
- ? (() => {
207
- const registry = this.config.toolRegistry.clone();
208
- registry.registerPlugin(
209
- 'studio_run',
210
- createStudioRunTool({
211
- spawner: this.config.spawner!,
212
- currentRunId: pipelineRun.id,
213
- currentDepth: input.depth ?? 0,
214
- maxDepth: this.config.maxDepth ?? 3,
215
- }),
216
- STUDIO_RUN_PROMPT_SNIPPET
217
- );
218
- return registry;
219
- })()
220
- : this.config.toolRegistry;
221
-
222
- this.events?.onPipelineStart?.({
223
- pipeline_name: pipeline.name,
224
- run_id: pipelineRun.id,
225
- });
226
- this.emitter.emit({ type: 'pipeline_start', pipelineId: pipelineRun.id });
227
-
228
- // 3. Initialize context
229
- const pipelineContext = createInitialContext(userInputValue, this.config.repoPath);
230
-
231
- // Run on_pipeline_start commands to bootstrap dynamic context
232
- if (pipeline.on_pipeline_start?.length) {
233
- const cwd = this.config.repoPath ?? this.config.configsDir;
234
- pipelineContext.startupContext = await executeStartupCommands(
235
- pipeline.on_pipeline_start,
236
- cwd
237
- );
238
- }
239
-
240
- // Load .studio/invariants.md if present — injected into every agent's system_prompt
241
- pipelineContext.invariantsContent = await loadInvariantsFile(projectPaths.projectDir);
242
-
243
- // Pre-populate context from prior run if resuming
244
- const skipSet: Set<string> =
245
- input.resumeFromStage
246
- ? buildSkipSet(pipeline.stages, input.resumeFromStage)
247
- : new Set();
248
-
249
- if (input.resumeFromStage) {
250
- for (const [stageName, output] of input.priorStageOutputs ?? []) {
251
- addStageOutput(pipelineContext, stageName, output);
252
- }
253
- for (const [stageName, toolCalls] of input.priorStageToolResults ?? []) {
254
- addStageToolResults(pipelineContext, stageName, toolCalls);
255
- }
256
- }
257
-
258
- // 4. Execute stages sequentially (handling groups)
259
- const totalStages = countTotalStages(pipeline.stages);
260
- let stageCounter = 0;
261
- let previousStageName: string | undefined;
262
-
263
- for (const entry of pipeline.stages) {
264
- // Check for cancellation before each pipeline entry
265
- if (signal?.aborted) {
266
- pipelineRun.status = 'cancelled';
267
- pipelineRun.completed_at = new Date().toISOString();
268
- const lastStage = pipelineRun.stages[pipelineRun.stages.length - 1];
269
- this.events?.onPipelineCancelled?.({
270
- run_id: pipelineRun.id,
271
- cancelled_at_stage: lastStage?.stage_name ?? 'before_first_stage',
272
- duration_ms: Date.now() - pipelineStartTime,
273
- });
274
- await this.config.db?.savePipelineRun(pipelineRun);
275
- this.events?.onPipelineComplete?.({
276
- pipeline_name: pipeline.name,
277
- run_id: pipelineRun.id,
278
- status: 'cancelled',
279
- duration_ms: Date.now() - pipelineStartTime,
280
- total_tokens: this.pipelineTotals.tokens,
281
- total_tool_calls: this.pipelineTotals.toolCalls,
282
- });
283
- this.emitter.emit({ type: 'pipeline_complete', pipelineId: pipelineRun.id });
284
- return pipelineRun;
285
- }
286
-
287
- if (isStageGroup(entry)) {
288
- // ========== GROUP ==========
289
-
290
- // Check if all group stages should be skipped
291
- const allGroupStagesSkipped = entry.stages.every(s => skipSet.has(s.name));
292
-
293
- if (allGroupStagesSkipped) {
294
- const groupSkipReason = input.originalRunId
295
- ? `resumed from run ${input.originalRunId}`
296
- : 'resumed from prior run';
297
- for (const stage of entry.stages) {
298
- stageCounter++;
299
- const skippedRun = makeSkippedStageRun(stage.name, groupSkipReason);
300
- this.events?.onStageComplete?.({
301
- stage_name: stage.name,
302
- stage_index: stageCounter - 1,
303
- total_stages: totalStages,
304
- status: 'skipped',
305
- attempts: 0,
306
- duration_ms: 0,
307
- skipped_reason: groupSkipReason,
308
- });
309
- pipelineRun.stages.push(skippedRun);
310
- previousStageName = stage.name;
311
- }
312
- clearGroupFeedback(pipelineContext);
313
- continue;
314
- }
315
-
316
- const groupResult = await this.groupOrchestrator.run(
317
- entry,
318
- pipelineContext,
319
- stageCounter,
320
- totalStages,
321
- userInputValue,
322
- projectPaths,
323
- runToolRegistry,
324
- runMiddleware,
325
- pipelineRun.id,
326
- signal,
327
- skipSet,
328
- input.originalRunId,
329
- );
330
- this.pipelineTotals.tokens += groupResult.totalTokensDelta;
331
- this.pipelineTotals.toolCalls += groupResult.totalToolCallsDelta;
332
-
333
- pipelineRun.stages.push(...groupResult.stageRuns);
334
- stageCounter += groupResult.stagesExecuted;
335
- // Update previousStageName to the last stage in the group
336
- if (entry.stages.length > 0) {
337
- previousStageName = entry.stages[entry.stages.length - 1].name;
338
- }
339
-
340
- // Clear group feedback after group completes
341
- clearGroupFeedback(pipelineContext);
342
-
343
- if (groupResult.status === 'rejected' || groupResult.status === 'failed' || groupResult.status === 'cancelled') {
344
- pipelineRun.status = groupResult.status;
345
- pipelineRun.completed_at = new Date().toISOString();
346
- if (groupResult.status === 'cancelled') {
347
- const lastStage = pipelineRun.stages[pipelineRun.stages.length - 1];
348
- this.events?.onPipelineCancelled?.({
349
- run_id: pipelineRun.id,
350
- cancelled_at_stage: lastStage?.stage_name ?? 'unknown',
351
- duration_ms: Date.now() - pipelineStartTime,
352
- });
353
- }
354
- await this.config.db?.savePipelineRun(pipelineRun);
355
- if (runMiddleware) {
356
- await this.persistKeymap(pipelineRun.id, runMiddleware.getKeymap());
357
- }
358
- this.events?.onPipelineComplete?.({
359
- pipeline_name: pipeline.name,
360
- run_id: pipelineRun.id,
361
- status: pipelineRun.status,
362
- duration_ms: Date.now() - pipelineStartTime,
363
- total_tokens: this.pipelineTotals.tokens,
364
- total_tool_calls: this.pipelineTotals.toolCalls,
365
- });
366
- this.emitter.emit({ type: 'pipeline_complete', pipelineId: pipelineRun.id });
367
- return pipelineRun;
368
- }
369
- } else {
370
- // ========== SIMPLE STAGE ==========
371
- stageCounter++;
372
-
373
- // Skip stage if resuming from a later stage
374
- if (skipSet.has(entry.name)) {
375
- const skippedReason = input.originalRunId
376
- ? `resumed from run ${input.originalRunId}`
377
- : 'resumed from prior run';
378
- const skippedRun = makeSkippedStageRun(entry.name, skippedReason);
379
- this.events?.onStageComplete?.({
380
- stage_name: entry.name,
381
- stage_index: stageCounter - 1,
382
- total_stages: totalStages,
383
- status: 'skipped',
384
- attempts: 0,
385
- duration_ms: 0,
386
- skipped_reason: skippedReason,
387
- });
388
- pipelineRun.stages.push(skippedRun);
389
- previousStageName = entry.name;
390
- continue;
391
- }
392
-
393
- const result = await this.stageExecutor.execute(
394
- entry,
395
- pipelineContext,
396
- previousStageName,
397
- userInputValue,
398
- stageCounter - 1,
399
- totalStages,
400
- projectPaths,
401
- runToolRegistry,
402
- runMiddleware,
403
- pipelineRun.id,
404
- signal,
405
- );
406
- this.pipelineTotals.tokens += result.tokensDelta ?? 0;
407
- this.pipelineTotals.toolCalls += result.toolCallsDelta ?? 0;
408
-
409
- pipelineRun.stages.push(result.stageRun);
410
-
411
- if (result.status === 'failed' || result.status === 'rejected' || result.status === 'cancelled') {
412
- pipelineRun.status = result.stageRun.status;
413
- pipelineRun.completed_at = new Date().toISOString();
414
- if (result.status === 'cancelled') {
415
- this.events?.onPipelineCancelled?.({
416
- run_id: pipelineRun.id,
417
- cancelled_at_stage: result.stageRun.stage_name,
418
- duration_ms: Date.now() - pipelineStartTime,
419
- });
420
- }
421
- await this.config.db?.savePipelineRun(pipelineRun);
422
- if (runMiddleware) {
423
- await this.persistKeymap(pipelineRun.id, runMiddleware.getKeymap());
424
- }
425
- this.events?.onPipelineComplete?.({
426
- pipeline_name: pipeline.name,
427
- run_id: pipelineRun.id,
428
- status: pipelineRun.status,
429
- duration_ms: Date.now() - pipelineStartTime,
430
- total_tokens: this.pipelineTotals.tokens,
431
- total_tool_calls: this.pipelineTotals.toolCalls,
432
- });
433
- this.emitter.emit({ type: 'pipeline_complete', pipelineId: pipelineRun.id });
434
- return pipelineRun;
435
- }
436
-
437
- if (result.lastAgentOutput !== undefined) {
438
- addStageOutput(pipelineContext, entry.name, result.lastAgentOutput);
439
- }
440
- if (result.toolCalls && result.toolCalls.length > 0) {
441
- addStageToolResults(pipelineContext, entry.name, result.toolCalls);
442
- }
443
-
444
- previousStageName = entry.name;
445
- }
446
- }
447
-
448
- // 5. All stages succeeded
449
- pipelineRun.status = 'success';
450
- pipelineRun.completed_at = new Date().toISOString();
451
-
452
- // 6. Persist then emit (DB must be updated before SSE fires, so spawnAndWait GET sees final status)
453
- await this.config.db?.savePipelineRun(pipelineRun);
454
- if (runMiddleware) {
455
- await this.persistKeymap(pipelineRun.id, runMiddleware.getKeymap());
456
- }
457
- this.events?.onPipelineComplete?.({
458
- pipeline_name: pipeline.name,
459
- run_id: pipelineRun.id,
460
- status: pipelineRun.status,
461
- duration_ms: Date.now() - pipelineStartTime,
462
- total_tokens: this.pipelineTotals.tokens,
463
- total_tool_calls: this.pipelineTotals.toolCalls,
464
- });
465
- this.emitter.emit({ type: 'pipeline_complete', pipelineId: pipelineRun.id });
466
-
467
- return pipelineRun;
468
- }
469
-
470
- /** For external listeners via the generic event bus */
471
- onEvent(listener: (event: import('./events.js').PipelineEvent) => void): void {
472
- this.emitter.on(listener);
473
- }
474
-
475
- // -- Private --
476
-
477
- private async persistKeymap(runId: string, keymap: Record<string, string>): Promise<void> {
478
- if (Object.keys(keymap).length === 0) return;
479
- try {
480
- const { mkdir, writeFile } = await import('node:fs/promises');
481
- const { join } = await import('node:path');
482
- // configsDir is .studio/ directly — keymap goes in .studio/runs/anonymization/
483
- const anonDir = join(this.config.configsDir, 'runs', 'anonymization');
484
- await mkdir(anonDir, { recursive: true });
485
- const keymapPath = join(anonDir, `${runId}.keymap.json`);
486
- await writeFile(keymapPath, JSON.stringify(keymap, null, 2), 'utf-8');
487
- } catch {
488
- // Non-fatal — keymap persistence is best-effort
489
- }
490
- }
491
- }
package/src/events.ts DELETED
@@ -1,167 +0,0 @@
1
- import type { ToolCall, ToolCallStartEvent, ToolCallCompleteEvent, AgentThinkingEvent, AgentProgressEvent, AgentTokenEvent } from '@studio-foundation/contracts';
2
-
3
- // Event types for pipeline observability
4
- // Dedicated event types — separate from contract types (PipelineRun, StageRun)
5
-
6
- export interface ToolCallSummary {
7
- name: string;
8
- arguments_summary: string;
9
- }
10
-
11
- export interface TokenUsage {
12
- prompt_tokens: number;
13
- completion_tokens: number;
14
- total_tokens: number;
15
- }
16
-
17
- export interface PipelineStartEvent {
18
- pipeline_name: string;
19
- run_id: string;
20
- }
21
-
22
- export interface PipelineCompleteEvent {
23
- pipeline_name: string;
24
- run_id: string;
25
- status: string;
26
- duration_ms: number;
27
- total_tokens: number;
28
- total_tool_calls: number;
29
- }
30
-
31
- export interface PipelineCancelledEvent {
32
- run_id: string;
33
- cancelled_at_stage: string;
34
- duration_ms: number;
35
- }
36
-
37
- export interface StageStartEvent {
38
- stage_name: string;
39
- stage_index: number;
40
- total_stages: number;
41
- max_attempts: number;
42
- }
43
-
44
- export interface StageCompleteEvent {
45
- stage_name: string;
46
- stage_index: number;
47
- total_stages: number;
48
- status: string;
49
- attempts: number;
50
- duration_ms: number;
51
- output_summary?: string;
52
- output?: unknown;
53
- tool_calls?: ToolCall[];
54
- token_usage?: TokenUsage;
55
- rejection_reason?: string;
56
- rejection_details?: string[];
57
- skipped_reason?: string;
58
- }
59
-
60
- export interface StageRetryEvent {
61
- stage: string;
62
- attempt: number;
63
- max_attempts: number;
64
- failures: string[];
65
- agent_output_raw?: string;
66
- tool_calls_count?: number;
67
- }
68
-
69
- export interface GroupStartEvent {
70
- group_name: string;
71
- max_iterations: number;
72
- parallel?: boolean;
73
- }
74
-
75
- export interface GroupIterationEvent {
76
- group_name: string;
77
- iteration: number;
78
- max_iterations: number;
79
- }
80
-
81
- export interface GroupFeedbackEvent {
82
- group_name: string;
83
- iteration: number;
84
- rejection_reason: string;
85
- rejection_details: string[];
86
- }
87
-
88
- export interface GroupCompleteEvent {
89
- group_name: string;
90
- iterations: number;
91
- status: string;
92
- }
93
-
94
- export interface StageContextEvent {
95
- stage: string;
96
- run_id: string;
97
- context_keys: Record<string, number>;
98
- context_content?: Record<string, unknown>;
99
- system_prompt?: string;
100
- }
101
-
102
- export interface StagedToolCallStartEvent extends ToolCallStartEvent {
103
- stage: string;
104
- }
105
-
106
- export interface StagedToolCallCompleteEvent extends ToolCallCompleteEvent {
107
- stage: string;
108
- }
109
-
110
- export interface StagedAgentThinkingEvent extends AgentThinkingEvent {
111
- stage: string;
112
- }
113
-
114
- export interface StagedAgentProgressEvent extends AgentProgressEvent {
115
- stage: string;
116
- }
117
-
118
- export interface StagedAgentTokenEvent extends AgentTokenEvent {
119
- stage: string;
120
- }
121
-
122
- export interface EngineEvents {
123
- onPipelineStart?: (event: PipelineStartEvent) => void;
124
- onPipelineComplete?: (event: PipelineCompleteEvent) => void;
125
- onPipelineCancelled?: (event: PipelineCancelledEvent) => void;
126
- onStageStart?: (event: StageStartEvent) => void;
127
- onStageComplete?: (event: StageCompleteEvent) => void;
128
- onTaskRetry?: (event: StageRetryEvent) => void;
129
- onGroupStart?: (event: GroupStartEvent) => void;
130
- onGroupIteration?: (event: GroupIterationEvent) => void;
131
- onGroupFeedback?: (event: GroupFeedbackEvent) => void;
132
- onGroupComplete?: (event: GroupCompleteEvent) => void;
133
- onStageContext?: (event: StageContextEvent) => void;
134
- // Real-time tool call streaming (used by --live mode)
135
- onToolCallStart?: (event: StagedToolCallStartEvent) => void;
136
- onToolCallComplete?: (event: StagedToolCallCompleteEvent) => void;
137
- // Agent thinking/progress (text content emitted alongside tool calls)
138
- onAgentThinking?: (event: StagedAgentThinkingEvent) => void;
139
- onAgentProgress?: (event: StagedAgentProgressEvent) => void;
140
- onAgentToken?: (event: StagedAgentTokenEvent) => void;
141
- }
142
-
143
- // Keep the generic event bus for other use cases
144
- export type PipelineEvent =
145
- | { type: 'pipeline_start'; pipelineId: string }
146
- | { type: 'pipeline_complete'; pipelineId: string }
147
- | { type: 'stage_start'; stageId: string; stageName: string }
148
- | { type: 'stage_complete'; stageId: string; stageName: string }
149
- | { type: 'task_retry'; stageName: string; attempt: number; failures: string[]; rawOutput?: string }
150
- | { type: 'group_start'; groupName: string; maxIterations: number }
151
- | { type: 'group_iteration'; groupName: string; iteration: number; maxIterations: number }
152
- | { type: 'group_feedback'; groupName: string; iteration: number; rejectionReason: string }
153
- | { type: 'group_complete'; groupName: string; iterations: number; status: string };
154
-
155
- export class PipelineEventEmitter {
156
- private listeners: Array<(event: PipelineEvent) => void> = [];
157
-
158
- on(listener: (event: PipelineEvent) => void): void {
159
- this.listeners.push(listener);
160
- }
161
-
162
- emit(event: PipelineEvent): void {
163
- for (const listener of this.listeners) {
164
- listener(event);
165
- }
166
- }
167
- }