@atolis-hq/wake 0.3.19 → 0.3.20
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.
- package/dist/src/activities/pr/application.js +5 -1
- package/dist/src/activities/pr/policy.js +10 -0
- package/dist/src/bootstrap/activity-registry.js +32 -0
- package/dist/src/bootstrap/composition-root.js +20 -244
- package/dist/src/bootstrap/index.js +2 -0
- package/dist/src/bootstrap/integration-runtime.js +184 -0
- package/dist/src/bootstrap/persistence-composition.js +19 -0
- package/dist/src/bootstrap/resource-transition-evidence.js +17 -0
- package/dist/src/bootstrap/resource-transition-ordering.js +67 -0
- package/dist/src/bootstrap/transcript-retention.js +29 -0
- package/dist/src/bootstrap/version.js +1 -1
- package/dist/src/orchestration/application/advance-workflow.js +12 -22
- package/dist/src/orchestration/application/orchestration-repository.js +19 -4
- package/dist/src/orchestration/application/orchestration-service.js +12 -2
- package/dist/src/orchestration/application/pull-request-transition-evidence.js +70 -0
- package/dist/src/orchestration/application/resource-transition-evidence.js +1 -0
- package/dist/src/orchestration/application/resource-transition-matching.js +72 -0
- package/dist/src/orchestration/application/resource-transition-reactor.js +65 -0
- package/dist/src/orchestration/application/watch-matching.js +27 -0
- package/dist/src/orchestration/contracts/config.js +24 -1
- package/dist/src/orchestration/contracts/event-decoder.js +18 -1
- package/dist/src/orchestration/contracts/events.js +1 -0
- package/dist/src/orchestration/domain/approval-defaults.js +1 -0
- package/dist/src/orchestration/domain/compiler.js +14 -1
- package/dist/src/orchestration/domain/resource-transition-compiler.js +12 -0
- package/dist/src/orchestration/domain/transition.js +18 -9
- package/dist/src/orchestration/domain/workflow-graph.js +5 -1
- package/dist/src/orchestration/domain/workflow-instance-events.js +3 -0
- package/dist/src/orchestration/index.js +4 -0
- package/dist/src/persistence/filesystem/file-lock.js +199 -27
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resourceStream, } from '../../resources/index.js';
|
|
2
2
|
import { workItemStream } from '../../work/index.js';
|
|
3
|
-
import { ActivityEventType } from '../contracts/events.js';
|
|
3
|
+
import { ActivityEventType, selectActivityEvent, } from '../contracts/events.js';
|
|
4
4
|
import { ActivityResourceRole } from '../contracts/vocabulary.js';
|
|
5
5
|
import { isReviewAuthorized } from '../review/authorization.js';
|
|
6
6
|
import { isPullRequestLikeResource } from './capability.js';
|
|
@@ -24,6 +24,10 @@ class JournalPullRequestService {
|
|
|
24
24
|
const events = await this.journal.readStream(resourceStream(resourceId));
|
|
25
25
|
return events.reduce((view, event) => pullRequestProjection.project(view, event), pullRequestProjection.initial(resourceId));
|
|
26
26
|
}
|
|
27
|
+
async factsFor(resourceId) {
|
|
28
|
+
const events = await this.journal.readStream(resourceStream(resourceId));
|
|
29
|
+
return events.map(selectActivityEvent).filter(isPresent);
|
|
30
|
+
}
|
|
27
31
|
async observe(command, context) {
|
|
28
32
|
if (!(await this.hasVerifiedPrimaryCorrelation(command)))
|
|
29
33
|
throw new Error('Pull request lacks verified primary correlation');
|
|
@@ -55,6 +55,16 @@ function hasPrimaryCorrelationConflict(resource, workItemId) {
|
|
|
55
55
|
primary.length !== 1 ||
|
|
56
56
|
primary[0].workItemId !== workItemId);
|
|
57
57
|
}
|
|
58
|
+
// Happy-path wrapper over the same primary-resource and pull-request
|
|
59
|
+
// selection rules `decidePullRequestAuthority` uses, for callers that only
|
|
60
|
+
// need the selected pair and not a specific denial code.
|
|
61
|
+
export function selectPrimaryPullRequest(input, workItemId) {
|
|
62
|
+
const resource = selectResource(input, workItemId, ActivityResourceRole.Primary);
|
|
63
|
+
if (isDenial(resource))
|
|
64
|
+
return null;
|
|
65
|
+
const pullRequest = selectPullRequest(input, resource, workItemId);
|
|
66
|
+
return isDenial(pullRequest) ? null : { resource, pullRequest };
|
|
67
|
+
}
|
|
58
68
|
function selectResource(input, workItemId, target) {
|
|
59
69
|
const resources = input.resources.filter((entry) => isPrimaryPullRequest(entry, workItemId) &&
|
|
60
70
|
(target === ActivityResourceRole.Primary || entry.resource.resourceId === target.resourceId));
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ActivityRegistry, agentActivityDefinition, createAgentActivity, createIssueCompleteActivity, createPullRequestApproveActivity, createPullRequestMergeActivity, } from '../activities/index.js';
|
|
2
|
+
import { loadPromptTemplate, renderPromptTemplate } from '../execution/index.js';
|
|
3
|
+
import { createStatusPublishActivity } from './status-publish-activity.js';
|
|
4
|
+
export function createBuiltInActivityRegistry(journal, pullRequests, resources, wakeRoot, contextReader) {
|
|
5
|
+
const activities = new ActivityRegistry();
|
|
6
|
+
activities.register({
|
|
7
|
+
...agentActivityDefinition,
|
|
8
|
+
handler: createAgentActivity({
|
|
9
|
+
async render(name, context) {
|
|
10
|
+
const template = await loadPromptTemplate(wakeRoot, name);
|
|
11
|
+
return {
|
|
12
|
+
prompt: renderPromptTemplate(template, context),
|
|
13
|
+
...(template.frontmatter.model === undefined || template.frontmatter.model === null
|
|
14
|
+
? {}
|
|
15
|
+
: { model: template.frontmatter.model }),
|
|
16
|
+
...(template.frontmatter.allowedTools === undefined ||
|
|
17
|
+
template.frontmatter.allowedTools === null
|
|
18
|
+
? {}
|
|
19
|
+
: { allowedTools: template.frontmatter.allowedTools }),
|
|
20
|
+
...(template.frontmatter.maxTurns === undefined
|
|
21
|
+
? {}
|
|
22
|
+
: { maxTurns: template.frontmatter.maxTurns }),
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
}, contextReader),
|
|
26
|
+
});
|
|
27
|
+
activities.register(createStatusPublishActivity(journal));
|
|
28
|
+
activities.register(createIssueCompleteActivity(journal, resources));
|
|
29
|
+
activities.register(createPullRequestApproveActivity(journal, pullRequests));
|
|
30
|
+
activities.register(createPullRequestMergeActivity(journal, pullRequests));
|
|
31
|
+
return activities;
|
|
32
|
+
}
|
|
@@ -1,25 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ControlStreamKind, DispatchPolicy,
|
|
3
|
-
import { ExternalExecutionState, GitWorkspaceProvider, RecoveryService,
|
|
4
|
-
import { AgentRunPublicationReactor, ArtifactRegistrationReactor, DeliveryOutcomeReactor, DeliveryService, IntegrationStreamKind, PollService, ProviderRegistry, fakeProviderDefinition, } from '../integrations/index.js';
|
|
5
|
-
import { EventActorKind, SystemClock, UlidIdGenerator, } from '../kernel/index.js';
|
|
6
|
-
import { compileWorkflow, compileWorkflowSelectors, createOrchestrationService, createWatchReactor, selectWorkflow, workflowName, } from '../orchestration/index.js';
|
|
7
|
-
import { FileCheckpointStore, FileEventJournal, FileProjectionStore, } from '../persistence/index.js';
|
|
8
|
-
import { createResourceLookup, createResourceService, resourceId, } from '../resources/index.js';
|
|
9
|
-
// The shared Integration barrel must not re-export a provider namespace
|
|
10
|
-
// (see provider-locality); composition-root is the exempt production
|
|
11
|
-
// composition point that is allowed to name it directly.
|
|
1
|
+
import { createPullRequestService } from '../activities/index.js';
|
|
2
|
+
import { ControlStreamKind, DispatchPolicy, createAdvanceOnce, createControlPlaneService, createRunnerControlService, ineligibleRunners, } from '../control-plane/index.js';
|
|
3
|
+
import { ExternalExecutionState, GitWorkspaceProvider, RecoveryService, TranscriptStore, createExecutionService, } from '../execution/index.js';
|
|
12
4
|
import { createGitHubAgentContextReader, gitHubProviderDefinition, resolveGitHubResourceUrl, } from '../integrations/github/index.js';
|
|
13
|
-
import {
|
|
5
|
+
import { SystemClock, UlidIdGenerator, } from '../kernel/index.js';
|
|
6
|
+
import { compileWorkflow, createOrchestrationService } from '../orchestration/index.js';
|
|
7
|
+
import { createResourceLookup, createResourceService, resourceId, } from '../resources/index.js';
|
|
8
|
+
import { createWorkService } from '../work/index.js';
|
|
9
|
+
import { createBuiltInActivityRegistry } from './activity-registry.js';
|
|
14
10
|
import { loadConfig } from './config/load-config.js';
|
|
15
|
-
import { hydrateFakeProviderEvidence } from './fake-provider-files.js';
|
|
16
11
|
import { loadFakeScenarios } from './fake-scenarios.js';
|
|
12
|
+
import { composeIntegrationRuntime } from './integration-runtime.js';
|
|
17
13
|
import { resolveWakePaths } from './paths.js';
|
|
18
|
-
import {
|
|
14
|
+
import { composePersistence } from './persistence-composition.js';
|
|
19
15
|
import { createRunnerQuotaReporter } from './runner-quota-reporter.js';
|
|
20
16
|
import { createRunnerRegistry } from './runner-registry.js';
|
|
21
17
|
import { FileScheduleCheckpointStore } from './schedule-checkpoint-store.js';
|
|
22
|
-
import {
|
|
18
|
+
import { createTranscriptRetention } from './transcript-retention.js';
|
|
23
19
|
import { createUpdateMaintenanceLease, } from './update-maintenance-lease.js';
|
|
24
20
|
const resourceLinkResolvers = {
|
|
25
21
|
github: resolveGitHubResourceUrl,
|
|
@@ -27,7 +23,6 @@ const resourceLinkResolvers = {
|
|
|
27
23
|
function resolveResourceLink(externalKey) {
|
|
28
24
|
return resourceLinkResolvers[externalKey.adapter]?.(externalKey) ?? null;
|
|
29
25
|
}
|
|
30
|
-
// Composition is deliberately the one place that assembles every module.
|
|
31
26
|
// eslint-disable-next-line complexity
|
|
32
27
|
export async function createCompositionRoot(wakeRoot, options = {}) {
|
|
33
28
|
const config = options.config ?? (await loadConfig(wakeRoot));
|
|
@@ -36,12 +31,13 @@ export async function createCompositionRoot(wakeRoot, options = {}) {
|
|
|
36
31
|
const maintenance = createUpdateMaintenanceLease(paths.wakeRoot);
|
|
37
32
|
const clock = options.clock ?? new SystemClock();
|
|
38
33
|
const ids = new UlidIdGenerator();
|
|
39
|
-
const { journal, projections, checkpoints } = composePersistence(paths, clock, options);
|
|
34
|
+
const { journal, projections, checkpoints, resourceTransitionOrdering, resourceTransitionTriggers, } = composePersistence(paths, clock, options);
|
|
40
35
|
const work = createWorkService(journal);
|
|
41
36
|
const lookup = createResourceLookup({ journal, projections });
|
|
42
37
|
const resources = createResourceService(journal, lookup);
|
|
43
38
|
const pullRequests = createPullRequestService(journal, work, resources);
|
|
44
|
-
const activities = options.activities ??
|
|
39
|
+
const activities = options.activities ??
|
|
40
|
+
createBuiltInActivityRegistry(journal, pullRequests, resources, wakeRoot, createGitHubAgentContextReader(journal, resources));
|
|
45
41
|
const definitions = Object.fromEntries(Object.entries(config.orchestration.workflows).map(([name, definition]) => [
|
|
46
42
|
name,
|
|
47
43
|
compileWorkflow(name, definition, activities, Object.keys(config.orchestration.workflows)),
|
|
@@ -76,9 +72,7 @@ export async function createCompositionRoot(wakeRoot, options = {}) {
|
|
|
76
72
|
});
|
|
77
73
|
const recovery = new RecoveryService(journal, clock, {
|
|
78
74
|
async inspect() {
|
|
79
|
-
//
|
|
80
|
-
// Preserve safety on restart: unknown external work is reconciled through
|
|
81
|
-
// the existing ambiguity path rather than being guessed as absent.
|
|
75
|
+
// Unknown external work follows the safe ambiguity path until runners expose inspection.
|
|
82
76
|
return {
|
|
83
77
|
kind: ExternalExecutionState.Unknown,
|
|
84
78
|
reason: 'External execution inspection is not configured for this runtime',
|
|
@@ -86,7 +80,7 @@ export async function createCompositionRoot(wakeRoot, options = {}) {
|
|
|
86
80
|
},
|
|
87
81
|
}, activities, config.execution, orchestration);
|
|
88
82
|
const controlPlane = createControlPlaneService({ journal, clock, ids });
|
|
89
|
-
const isRuntimePaused = async () => (await controlPlane.isPaused()) || (await
|
|
83
|
+
const isRuntimePaused = async () => (await controlPlane.isPaused()) || (await maintenance.read()) !== null;
|
|
90
84
|
const runnerControls = createRunnerControlService({
|
|
91
85
|
journal,
|
|
92
86
|
clock,
|
|
@@ -104,29 +98,7 @@ export async function createCompositionRoot(wakeRoot, options = {}) {
|
|
|
104
98
|
work,
|
|
105
99
|
...(transcriptStore === undefined
|
|
106
100
|
? {}
|
|
107
|
-
:
|
|
108
|
-
transcriptRetention: {
|
|
109
|
-
async markClosedWorkItem(workItemId) {
|
|
110
|
-
try {
|
|
111
|
-
await transcriptStore.markWorkItemCleaned(workItemId, config.transcripts.retentionMs, clock.now().toISOString());
|
|
112
|
-
return true;
|
|
113
|
-
}
|
|
114
|
-
catch (error) {
|
|
115
|
-
console.error('Transcript retention failed', error);
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
async sweep() {
|
|
120
|
-
try {
|
|
121
|
-
await transcriptStore.sweepExpired(config.transcripts.retentionMs, clock.now().toISOString(), (_workItemId, error) => console.error('Transcript retention failed', error));
|
|
122
|
-
}
|
|
123
|
-
catch (error) {
|
|
124
|
-
console.error('Transcript retention failed', error);
|
|
125
|
-
}
|
|
126
|
-
},
|
|
127
|
-
},
|
|
128
|
-
closedWorkItemIds: () => closedWorkItemIds(projections),
|
|
129
|
-
}),
|
|
101
|
+
: createTranscriptRetention(transcriptStore, projections, config, clock)),
|
|
130
102
|
runnerIneligibility: async () => {
|
|
131
103
|
const stored = await projections.read(ControlStreamKind.Global, 'global');
|
|
132
104
|
return stored === null
|
|
@@ -153,12 +125,15 @@ export async function createCompositionRoot(wakeRoot, options = {}) {
|
|
|
153
125
|
ids,
|
|
154
126
|
wakeRoot,
|
|
155
127
|
scheduleCheckpoints: options.scheduleCheckpoints ?? new FileScheduleCheckpointStore(paths.dataRoot),
|
|
128
|
+
resourceTransitionOrdering,
|
|
129
|
+
resourceTransitionTriggers,
|
|
156
130
|
...(options.decorateDeliveryAdapter === undefined
|
|
157
131
|
? {}
|
|
158
132
|
: { decorateDeliveryAdapter: options.decorateDeliveryAdapter }),
|
|
159
133
|
...(options.fakeDeliveryProvider === undefined
|
|
160
134
|
? {}
|
|
161
135
|
: { fakeDeliveryProvider: options.fakeDeliveryProvider }),
|
|
136
|
+
providerDefinitions: [gitHubProviderDefinition],
|
|
162
137
|
});
|
|
163
138
|
return {
|
|
164
139
|
config,
|
|
@@ -183,202 +158,3 @@ export async function createCompositionRoot(wakeRoot, options = {}) {
|
|
|
183
158
|
...runtime,
|
|
184
159
|
};
|
|
185
160
|
}
|
|
186
|
-
async function maintenanceBlocksRuntime(maintenance) {
|
|
187
|
-
return (await maintenance.read()) !== null;
|
|
188
|
-
}
|
|
189
|
-
async function closedWorkItemIds(projections) {
|
|
190
|
-
return (await projections.list('work')).flatMap(({ value }) => value?.state === WorkStatus.Closed ? [value.workItemId] : []);
|
|
191
|
-
}
|
|
192
|
-
function serializeRunRegisteredOnce(runner) {
|
|
193
|
-
let queue = Promise.resolve();
|
|
194
|
-
const runRegisteredOnce = runner.runRegisteredOnce.bind(runner);
|
|
195
|
-
runner.runRegisteredOnce = (limit) => {
|
|
196
|
-
const result = queue.then(() => runRegisteredOnce(limit));
|
|
197
|
-
queue = result.catch(() => { });
|
|
198
|
-
return result;
|
|
199
|
-
};
|
|
200
|
-
return runner;
|
|
201
|
-
}
|
|
202
|
-
async function composeIntegrationRuntime(input) {
|
|
203
|
-
const registry = new ProviderRegistry();
|
|
204
|
-
registry.register(fakeProviderDefinition);
|
|
205
|
-
registry.register(gitHubProviderDefinition);
|
|
206
|
-
const { instances, failures: providerFailures } = registry.compose(await hydrateFakeProviderEvidence(input.wakeRoot, input.config.integrations), {
|
|
207
|
-
publicUiUrl: input.config.surfaces.web.publicUrl,
|
|
208
|
-
work: input.work,
|
|
209
|
-
resources: input.resources,
|
|
210
|
-
resourceLookup: input.lookup,
|
|
211
|
-
pullRequests: input.pullRequests,
|
|
212
|
-
runs: new RunRepository(input.journal),
|
|
213
|
-
orchestration: input.orchestration,
|
|
214
|
-
ids: input.ids,
|
|
215
|
-
clock: input.clock,
|
|
216
|
-
journal: input.journal,
|
|
217
|
-
checkpoints: input.checkpoints,
|
|
218
|
-
routing: createWorkflowRouter(input.config.orchestration),
|
|
219
|
-
conclusion: createWorkCancellationPolicy(input.work, input.orchestration, input.execution, input.clock, input.ids),
|
|
220
|
-
});
|
|
221
|
-
const composedProviders = input.fakeDeliveryProvider === undefined
|
|
222
|
-
? instances
|
|
223
|
-
: instances.map((provider) => provider.adapter === 'fake'
|
|
224
|
-
? { ...provider, delivery: input.fakeDeliveryProvider }
|
|
225
|
-
: provider);
|
|
226
|
-
const providers = input.decorateDeliveryAdapter
|
|
227
|
-
? composedProviders.map((provider) => ({
|
|
228
|
-
...provider,
|
|
229
|
-
delivery: input.decorateDeliveryAdapter(provider.delivery, provider),
|
|
230
|
-
}))
|
|
231
|
-
: composedProviders;
|
|
232
|
-
// FileCheckpointStore.save throws on any regression (persistence/filesystem/
|
|
233
|
-
// file-checkpoint-store.ts), so two concurrent runRegisteredOnce calls that
|
|
234
|
-
// interleave can race a slower caller's stale checkpoint save against a
|
|
235
|
-
// faster one that already advanced past it. Every caller (the tick
|
|
236
|
-
// pipeline's own catchUpProjections, the API's manual tick, and the
|
|
237
|
-
// resident's standalone projection pump) shares this one instance, so
|
|
238
|
-
// serializing it here in-process covers all of them without a file lock.
|
|
239
|
-
const projectionRunner = serializeRunRegisteredOnce(createRuntimeProjectionRunner(input.journal, input.projections, input.checkpoints));
|
|
240
|
-
const delivery = new DeliveryService({
|
|
241
|
-
journal: input.journal,
|
|
242
|
-
intents: async () => (await input.projections.list(IntegrationStreamKind.Delivery)).map(({ value }) => value),
|
|
243
|
-
resource: async (id) => {
|
|
244
|
-
const resource = await input.resources.get(resourceId(id));
|
|
245
|
-
return resource === null
|
|
246
|
-
? null
|
|
247
|
-
: { resourceId: resource.resourceId, adapter: resource.externalKey.adapter };
|
|
248
|
-
},
|
|
249
|
-
adapter: (name) => {
|
|
250
|
-
const provider = providers.find((candidate) => candidate.adapter === name);
|
|
251
|
-
if (provider === undefined)
|
|
252
|
-
throw new Error(`Delivery provider ${name} is not configured`);
|
|
253
|
-
return provider.delivery;
|
|
254
|
-
},
|
|
255
|
-
now: () => input.clock.now().toISOString(),
|
|
256
|
-
});
|
|
257
|
-
const schedules = new ScheduleService({
|
|
258
|
-
checkpoint: input.scheduleCheckpoints,
|
|
259
|
-
ids: input.ids,
|
|
260
|
-
work: input.work,
|
|
261
|
-
orchestration: input.orchestration,
|
|
262
|
-
now: () => input.clock.now().toISOString(),
|
|
263
|
-
});
|
|
264
|
-
const artifacts = new ArtifactRegistrationReactor({
|
|
265
|
-
journal: input.journal,
|
|
266
|
-
checkpoints: input.checkpoints,
|
|
267
|
-
resources: input.resources,
|
|
268
|
-
ids: input.ids,
|
|
269
|
-
providers,
|
|
270
|
-
runs: input.execution,
|
|
271
|
-
});
|
|
272
|
-
const runs = new RunRepository(input.journal);
|
|
273
|
-
const agentRunPublications = new AgentRunPublicationReactor({
|
|
274
|
-
journal: input.journal,
|
|
275
|
-
checkpoints: input.checkpoints,
|
|
276
|
-
runs,
|
|
277
|
-
resources: input.resources,
|
|
278
|
-
orchestration: input.orchestration,
|
|
279
|
-
});
|
|
280
|
-
const watch = createWatchReactor(input.orchestration, input.journal, input.checkpoints, runs);
|
|
281
|
-
const outcomes = new DeliveryOutcomeReactor(input.journal, input.checkpoints, input.orchestration);
|
|
282
|
-
const catchUpProjections = async () => {
|
|
283
|
-
await projectionRunner.runRegisteredOnce();
|
|
284
|
-
};
|
|
285
|
-
// Only poll hits a rate-limited external API, so only this half of the
|
|
286
|
-
// tick needs a backing-off host — see bootstrap/surface-cli-applications.ts.
|
|
287
|
-
const intakePipeline = createIntakePipeline({
|
|
288
|
-
isPaused: input.isPaused,
|
|
289
|
-
catchUpProjections,
|
|
290
|
-
poll: async (signal) => {
|
|
291
|
-
let appended = 0;
|
|
292
|
-
for (const provider of providers)
|
|
293
|
-
appended += await new PollService(input.journal, provider).pollOnce(signal);
|
|
294
|
-
return appended;
|
|
295
|
-
},
|
|
296
|
-
translateInbound: async () => {
|
|
297
|
-
let translated = 0;
|
|
298
|
-
for (const provider of providers)
|
|
299
|
-
translated += await provider.inbound.runOnce();
|
|
300
|
-
return translated;
|
|
301
|
-
},
|
|
302
|
-
});
|
|
303
|
-
const runnerPipeline = createRunnerPipeline({
|
|
304
|
-
isPaused: input.isPaused,
|
|
305
|
-
catchUpProjections,
|
|
306
|
-
runSchedules: async () => {
|
|
307
|
-
for (const schedule of input.config.controlPlane.schedules)
|
|
308
|
-
await schedules.run(schedule, {
|
|
309
|
-
commandId: input.ids.next('command'),
|
|
310
|
-
correlationId: 'schedule-tick',
|
|
311
|
-
occurredAt: input.clock.now().toISOString(),
|
|
312
|
-
actor: { kind: EventActorKind.System, id: ControlStreamKind.Global },
|
|
313
|
-
});
|
|
314
|
-
},
|
|
315
|
-
react: async () => {
|
|
316
|
-
await watch.runOnce();
|
|
317
|
-
await artifacts.runOnce();
|
|
318
|
-
await outcomes.runOnce();
|
|
319
|
-
for (const provider of providers)
|
|
320
|
-
await provider.maintenance?.runOnce();
|
|
321
|
-
},
|
|
322
|
-
advance: input.advanceOnce,
|
|
323
|
-
publishAgentRuns: async () => {
|
|
324
|
-
await agentRunPublications.runOnce();
|
|
325
|
-
},
|
|
326
|
-
deliver: async (signal) => {
|
|
327
|
-
await delivery.deliverNext(signal);
|
|
328
|
-
},
|
|
329
|
-
});
|
|
330
|
-
return {
|
|
331
|
-
projectionRunner,
|
|
332
|
-
providers,
|
|
333
|
-
providerFailures,
|
|
334
|
-
delivery,
|
|
335
|
-
intakePipeline,
|
|
336
|
-
runnerPipeline,
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
function identity(value) {
|
|
340
|
-
return value;
|
|
341
|
-
}
|
|
342
|
-
function composePersistence(paths, clock, options) {
|
|
343
|
-
return {
|
|
344
|
-
journal: (options.decorateJournal ?? identity)(options.journal ?? new FileEventJournal(paths.dataRoot, clock)),
|
|
345
|
-
projections: (options.decorateProjections ?? identity)(options.projections ?? new FileProjectionStore(paths.dataRoot)),
|
|
346
|
-
checkpoints: (options.decorateCheckpoints ?? identity)(options.checkpoints ?? new FileCheckpointStore(paths.dataRoot)),
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
function createBuiltInActivityRegistry(journal, pullRequests, resources, wakeRoot) {
|
|
350
|
-
const activities = new ActivityRegistry();
|
|
351
|
-
const contextReader = createGitHubAgentContextReader(journal, resources);
|
|
352
|
-
activities.register({
|
|
353
|
-
...agentActivityDefinition,
|
|
354
|
-
handler: createAgentActivity({
|
|
355
|
-
async render(name, context) {
|
|
356
|
-
const template = await loadPromptTemplate(wakeRoot, name);
|
|
357
|
-
return {
|
|
358
|
-
prompt: renderPromptTemplate(template, context),
|
|
359
|
-
...(template.frontmatter.model === undefined || template.frontmatter.model === null
|
|
360
|
-
? {}
|
|
361
|
-
: { model: template.frontmatter.model }),
|
|
362
|
-
...(template.frontmatter.allowedTools === undefined ||
|
|
363
|
-
template.frontmatter.allowedTools === null
|
|
364
|
-
? {}
|
|
365
|
-
: { allowedTools: template.frontmatter.allowedTools }),
|
|
366
|
-
...(template.frontmatter.maxTurns === undefined
|
|
367
|
-
? {}
|
|
368
|
-
: { maxTurns: template.frontmatter.maxTurns }),
|
|
369
|
-
};
|
|
370
|
-
},
|
|
371
|
-
}, contextReader),
|
|
372
|
-
});
|
|
373
|
-
activities.register(createStatusPublishActivity(journal));
|
|
374
|
-
activities.register(createIssueCompleteActivity(journal, resources));
|
|
375
|
-
activities.register(createPullRequestApproveActivity(journal, pullRequests));
|
|
376
|
-
activities.register(createPullRequestMergeActivity(journal, pullRequests));
|
|
377
|
-
return activities;
|
|
378
|
-
}
|
|
379
|
-
// Configuration is the only routing authority: adapters ask, they never propose.
|
|
380
|
-
function createWorkflowRouter(orchestration) {
|
|
381
|
-
const selectors = compileWorkflowSelectors(orchestration.workflowSelectors);
|
|
382
|
-
const fallback = workflowName(orchestration.default);
|
|
383
|
-
return { select: (candidate) => selectWorkflow(candidate, selectors, fallback) };
|
|
384
|
-
}
|
|
@@ -32,6 +32,8 @@ export function composeControlPlaneHosts(advanceOnce, sleep) {
|
|
|
32
32
|
return { tick, resident: new ResidentHost(tick, sleep) };
|
|
33
33
|
}
|
|
34
34
|
export * from './composition-root.js';
|
|
35
|
+
export * from './resource-transition-evidence.js';
|
|
36
|
+
export * from './resource-transition-ordering.js';
|
|
35
37
|
export * from './analytics-projection.js';
|
|
36
38
|
export * from './board-projection.js';
|
|
37
39
|
export * from './config/load-config.js';
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { ControlStreamKind, ScheduleService, createIntakePipeline, createRunnerPipeline, createWorkCancellationPolicy, } from '../control-plane/index.js';
|
|
2
|
+
import { RunRepository } from '../execution/index.js';
|
|
3
|
+
import { AgentRunPublicationReactor, ArtifactRegistrationReactor, DeliveryOutcomeReactor, DeliveryService, IntegrationStreamKind, PollService, ProviderRegistry, fakeProviderDefinition, } from '../integrations/index.js';
|
|
4
|
+
import { EventActorKind, } from '../kernel/index.js';
|
|
5
|
+
import { compileWorkflowSelectors, createPullRequestTransitionEvidence, createResourceTransitionReactor, createWatchReactor, selectWorkflow, workflowName, } from '../orchestration/index.js';
|
|
6
|
+
import { BuiltInResourceCapability, resourceId, } from '../resources/index.js';
|
|
7
|
+
import { hydrateFakeProviderEvidence } from './fake-provider-files.js';
|
|
8
|
+
import { createRuntimeProjectionRunner } from './projection-runtime.js';
|
|
9
|
+
import { createCapabilityResourceTransitionEvidence } from './resource-transition-evidence.js';
|
|
10
|
+
function serializeRunRegisteredOnce(runner) {
|
|
11
|
+
let queue = Promise.resolve();
|
|
12
|
+
const runRegisteredOnce = runner.runRegisteredOnce.bind(runner);
|
|
13
|
+
runner.runRegisteredOnce = (limit) => {
|
|
14
|
+
const result = queue.then(() => runRegisteredOnce(limit));
|
|
15
|
+
queue = result.catch(() => { });
|
|
16
|
+
return result;
|
|
17
|
+
};
|
|
18
|
+
return runner;
|
|
19
|
+
}
|
|
20
|
+
export async function composeIntegrationRuntime(input) {
|
|
21
|
+
const registry = new ProviderRegistry();
|
|
22
|
+
registry.register(fakeProviderDefinition);
|
|
23
|
+
for (const definition of input.providerDefinitions)
|
|
24
|
+
registry.register(definition);
|
|
25
|
+
const { instances, failures: providerFailures } = registry.compose(await hydrateFakeProviderEvidence(input.wakeRoot, input.config.integrations), {
|
|
26
|
+
publicUiUrl: input.config.surfaces.web.publicUrl,
|
|
27
|
+
work: input.work,
|
|
28
|
+
resources: input.resources,
|
|
29
|
+
resourceLookup: input.lookup,
|
|
30
|
+
pullRequests: input.pullRequests,
|
|
31
|
+
runs: new RunRepository(input.journal),
|
|
32
|
+
orchestration: input.orchestration,
|
|
33
|
+
ids: input.ids,
|
|
34
|
+
clock: input.clock,
|
|
35
|
+
journal: input.journal,
|
|
36
|
+
checkpoints: input.checkpoints,
|
|
37
|
+
routing: createWorkflowRouter(input.config.orchestration),
|
|
38
|
+
conclusion: createWorkCancellationPolicy(input.work, input.orchestration, input.execution, input.clock, input.ids),
|
|
39
|
+
});
|
|
40
|
+
const composedProviders = input.fakeDeliveryProvider === undefined
|
|
41
|
+
? instances
|
|
42
|
+
: instances.map((provider) => provider.adapter === 'fake'
|
|
43
|
+
? { ...provider, delivery: input.fakeDeliveryProvider }
|
|
44
|
+
: provider);
|
|
45
|
+
const providers = input.decorateDeliveryAdapter
|
|
46
|
+
? composedProviders.map((provider) => ({
|
|
47
|
+
...provider,
|
|
48
|
+
delivery: input.decorateDeliveryAdapter(provider.delivery, provider),
|
|
49
|
+
}))
|
|
50
|
+
: composedProviders;
|
|
51
|
+
// FileCheckpointStore.save throws on any regression (persistence/filesystem/
|
|
52
|
+
// file-checkpoint-store.ts), so two concurrent runRegisteredOnce calls that
|
|
53
|
+
// interleave can race a slower caller's stale checkpoint save against a
|
|
54
|
+
// faster one that already advanced past it. Every caller (the tick
|
|
55
|
+
// pipeline's own catchUpProjections, the API's manual tick, and the
|
|
56
|
+
// resident's standalone projection pump) shares this one instance, so
|
|
57
|
+
// serializing it here in-process covers all of them without a file lock.
|
|
58
|
+
const projectionRunner = serializeRunRegisteredOnce(createRuntimeProjectionRunner(input.journal, input.projections, input.checkpoints));
|
|
59
|
+
const delivery = new DeliveryService({
|
|
60
|
+
journal: input.journal,
|
|
61
|
+
intents: async () => (await input.projections.list(IntegrationStreamKind.Delivery)).map(({ value }) => value),
|
|
62
|
+
resource: async (id) => {
|
|
63
|
+
const resource = await input.resources.get(resourceId(id));
|
|
64
|
+
return resource === null
|
|
65
|
+
? null
|
|
66
|
+
: { resourceId: resource.resourceId, adapter: resource.externalKey.adapter };
|
|
67
|
+
},
|
|
68
|
+
adapter: (name) => {
|
|
69
|
+
const provider = providers.find((candidate) => candidate.adapter === name);
|
|
70
|
+
if (provider === undefined)
|
|
71
|
+
throw new Error(`Delivery provider ${name} is not configured`);
|
|
72
|
+
return provider.delivery;
|
|
73
|
+
},
|
|
74
|
+
now: () => input.clock.now().toISOString(),
|
|
75
|
+
});
|
|
76
|
+
const schedules = new ScheduleService({
|
|
77
|
+
checkpoint: input.scheduleCheckpoints,
|
|
78
|
+
ids: input.ids,
|
|
79
|
+
work: input.work,
|
|
80
|
+
orchestration: input.orchestration,
|
|
81
|
+
now: () => input.clock.now().toISOString(),
|
|
82
|
+
});
|
|
83
|
+
const artifacts = new ArtifactRegistrationReactor({
|
|
84
|
+
journal: input.journal,
|
|
85
|
+
checkpoints: input.checkpoints,
|
|
86
|
+
resources: input.resources,
|
|
87
|
+
ids: input.ids,
|
|
88
|
+
providers,
|
|
89
|
+
runs: input.execution,
|
|
90
|
+
});
|
|
91
|
+
const runs = new RunRepository(input.journal);
|
|
92
|
+
const agentRunPublications = new AgentRunPublicationReactor({
|
|
93
|
+
journal: input.journal,
|
|
94
|
+
checkpoints: input.checkpoints,
|
|
95
|
+
runs,
|
|
96
|
+
resources: input.resources,
|
|
97
|
+
orchestration: input.orchestration,
|
|
98
|
+
});
|
|
99
|
+
const watch = createWatchReactor(input.orchestration, input.journal, input.checkpoints, runs);
|
|
100
|
+
const resourceTransitionEvidence = createCapabilityResourceTransitionEvidence({
|
|
101
|
+
resources: input.resources,
|
|
102
|
+
policies: [
|
|
103
|
+
{
|
|
104
|
+
capabilities: [
|
|
105
|
+
BuiltInResourceCapability.Mergeable,
|
|
106
|
+
BuiltInResourceCapability.Reviewable,
|
|
107
|
+
BuiltInResourceCapability.Approvable,
|
|
108
|
+
],
|
|
109
|
+
policy: createPullRequestTransitionEvidence(input.pullRequests),
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
});
|
|
113
|
+
input.resourceTransitionTriggers.register(resourceTransitionEvidence.triggers);
|
|
114
|
+
input.resourceTransitionTriggers.freeze();
|
|
115
|
+
const resourceTransitions = createResourceTransitionReactor(input.orchestration, resourceTransitionEvidence, input.journal, input.checkpoints, input.resourceTransitionOrdering);
|
|
116
|
+
input.orchestration.setAcceptSignalOperationCoordinator((operation) => input.resourceTransitionOrdering(async () => {
|
|
117
|
+
await resourceTransitions.drain();
|
|
118
|
+
return operation();
|
|
119
|
+
}));
|
|
120
|
+
const outcomes = new DeliveryOutcomeReactor(input.journal, input.checkpoints, input.orchestration);
|
|
121
|
+
const catchUpProjections = async () => {
|
|
122
|
+
await projectionRunner.runRegisteredOnce();
|
|
123
|
+
};
|
|
124
|
+
// Only poll hits a rate-limited external API, so only this half of the
|
|
125
|
+
// Tick needs a backing-off host; see bootstrap/surface-cli-applications.ts.
|
|
126
|
+
const intakePipeline = createIntakePipeline({
|
|
127
|
+
isPaused: input.isPaused,
|
|
128
|
+
catchUpProjections,
|
|
129
|
+
poll: async (signal) => {
|
|
130
|
+
let appended = 0;
|
|
131
|
+
for (const provider of providers)
|
|
132
|
+
appended += await new PollService(input.journal, provider).pollOnce(signal);
|
|
133
|
+
return appended;
|
|
134
|
+
},
|
|
135
|
+
translateInbound: async () => {
|
|
136
|
+
let translated = 0;
|
|
137
|
+
for (const provider of providers)
|
|
138
|
+
translated += await provider.inbound.runOnce();
|
|
139
|
+
return translated;
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
const runnerPipeline = createRunnerPipeline({
|
|
143
|
+
isPaused: input.isPaused,
|
|
144
|
+
catchUpProjections,
|
|
145
|
+
runSchedules: async () => {
|
|
146
|
+
for (const schedule of input.config.controlPlane.schedules)
|
|
147
|
+
await schedules.run(schedule, {
|
|
148
|
+
commandId: input.ids.next('command'),
|
|
149
|
+
correlationId: 'schedule-tick',
|
|
150
|
+
occurredAt: input.clock.now().toISOString(),
|
|
151
|
+
actor: { kind: EventActorKind.System, id: ControlStreamKind.Global },
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
react: async () => {
|
|
155
|
+
await watch.runOnce();
|
|
156
|
+
await resourceTransitions.runOnce();
|
|
157
|
+
await artifacts.runOnce();
|
|
158
|
+
await outcomes.runOnce();
|
|
159
|
+
for (const provider of providers)
|
|
160
|
+
await provider.maintenance?.runOnce();
|
|
161
|
+
},
|
|
162
|
+
advance: input.advanceOnce,
|
|
163
|
+
publishAgentRuns: async () => {
|
|
164
|
+
await agentRunPublications.runOnce();
|
|
165
|
+
},
|
|
166
|
+
deliver: async (signal) => {
|
|
167
|
+
await delivery.deliverNext(signal);
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
return {
|
|
171
|
+
projectionRunner,
|
|
172
|
+
providers,
|
|
173
|
+
providerFailures,
|
|
174
|
+
delivery,
|
|
175
|
+
intakePipeline,
|
|
176
|
+
runnerPipeline,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
// Configuration is the only routing authority: adapters ask, they never propose.
|
|
180
|
+
function createWorkflowRouter(orchestration) {
|
|
181
|
+
const selectors = compileWorkflowSelectors(orchestration.workflowSelectors);
|
|
182
|
+
const fallback = workflowName(orchestration.default);
|
|
183
|
+
return { select: (candidate) => selectWorkflow(candidate, selectors, fallback) };
|
|
184
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { FileCheckpointStore, FileEventJournal, FileProjectionStore, } from '../persistence/index.js';
|
|
2
|
+
import { createResourceTransitionOrdering, createTriggerAwareEventJournal, createTriggerRegistry, } from './resource-transition-ordering.js';
|
|
3
|
+
function identity(value) {
|
|
4
|
+
return value;
|
|
5
|
+
}
|
|
6
|
+
export function composePersistence(paths, clock, options) {
|
|
7
|
+
const resourceTransitionOrdering = createResourceTransitionOrdering(options.journal === undefined
|
|
8
|
+
? `${paths.locksRoot}/resource-transition-ordering.lock`
|
|
9
|
+
: undefined);
|
|
10
|
+
const resourceTransitionTriggers = createTriggerRegistry();
|
|
11
|
+
const journal = createTriggerAwareEventJournal((options.decorateJournal ?? identity)(options.journal ?? new FileEventJournal(paths.dataRoot, clock)), resourceTransitionTriggers, resourceTransitionOrdering);
|
|
12
|
+
return {
|
|
13
|
+
journal,
|
|
14
|
+
projections: (options.decorateProjections ?? identity)(options.projections ?? new FileProjectionStore(paths.dataRoot)),
|
|
15
|
+
checkpoints: (options.decorateCheckpoints ?? identity)(options.checkpoints ?? new FileCheckpointStore(paths.dataRoot)),
|
|
16
|
+
resourceTransitionOrdering,
|
|
17
|
+
resourceTransitionTriggers,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ResourceCorrelationRole } from '../resources/index.js';
|
|
2
|
+
export function createCapabilityResourceTransitionEvidence(input) {
|
|
3
|
+
return {
|
|
4
|
+
triggers: [...new Set(input.policies.flatMap(({ policy }) => policy.triggers))],
|
|
5
|
+
async resolve(evidence) {
|
|
6
|
+
const correlations = await input.resources.correlationsForWork(evidence.workItemId);
|
|
7
|
+
const primaries = correlations.filter(({ role }) => role === ResourceCorrelationRole.Primary);
|
|
8
|
+
if (primaries.length !== 1)
|
|
9
|
+
return null;
|
|
10
|
+
const resource = await input.resources.get(primaries[0].resourceId);
|
|
11
|
+
if (resource === null)
|
|
12
|
+
return null;
|
|
13
|
+
const registration = input.policies.find(({ capabilities }) => capabilities.some((capability) => resource.capabilities.includes(capability)));
|
|
14
|
+
return registration?.policy.resolve(evidence) ?? null;
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|