@atolis-hq/wake 0.4.39 → 0.4.40
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/agent/agent-result.d.ts +2 -0
- package/dist/src/activities/agent/agent-result.js +8 -0
- package/dist/src/bootstrap/version.js +1 -1
- package/dist/src/integrations/github/infrastructure/comment-source.js +7 -4
- package/dist/src/integrations/github/infrastructure/poll-watermark.d.ts +24 -0
- package/dist/src/integrations/github/infrastructure/poll-watermark.js +37 -1
- package/dist/src/integrations/github/infrastructure/pr-evidence.d.ts +18 -0
- package/dist/src/integrations/github/infrastructure/pr-evidence.js +32 -0
- package/dist/src/integrations/github/infrastructure/pr-source.d.ts +6 -2
- package/dist/src/integrations/github/infrastructure/pr-source.js +27 -38
- package/dist/src/integrations/github/infrastructure/source.js +33 -25
- package/dist/src/orchestration/application/advance-workflow.d.ts +1 -0
- package/dist/src/orchestration/application/advance-workflow.js +7 -1
- package/dist/src/orchestration/application/conversation-command-reactor.js +5 -2
- package/dist/src/orchestration/application/orchestration-service.d.ts +1 -0
- package/dist/src/orchestration/application/orchestration-service.js +3 -0
- package/dist/src/orchestration/domain/interpreter.d.ts +1 -1
- package/dist/src/orchestration/domain/interpreter.js +1 -1
- package/dist/src/orchestration/domain/operator-retry-policy.d.ts +2 -0
- package/dist/src/orchestration/domain/operator-retry-policy.js +15 -9
- package/package.json +1 -1
|
@@ -39,5 +39,7 @@ export declare const agentActivityOutcomeSchema: z.ZodUnion<readonly [z.ZodObjec
|
|
|
39
39
|
}, z.core.$strict>]>;
|
|
40
40
|
}, z.core.$strict>]>;
|
|
41
41
|
export type AgentActivityOutcome = z.output<typeof agentActivityOutcomeSchema>;
|
|
42
|
+
/** Identifies the agent sentinel that explicitly asks a human for input. */
|
|
43
|
+
export declare function isNeedsClarificationAgentOutcome(value: unknown): boolean;
|
|
42
44
|
export declare const agentActivityOutcomeKinds: readonly ["done", "rejected", "blocked", "failed"];
|
|
43
45
|
export declare function translateAgentResult(value: unknown): AgentActivityOutcome;
|
|
@@ -52,6 +52,14 @@ export const agentActivityOutcomeSchema = z.union([
|
|
|
52
52
|
})
|
|
53
53
|
.strict(),
|
|
54
54
|
]);
|
|
55
|
+
/** Identifies the agent sentinel that explicitly asks a human for input. */
|
|
56
|
+
export function isNeedsClarificationAgentOutcome(value) {
|
|
57
|
+
const parsed = agentActivityOutcomeSchema.safeParse(value);
|
|
58
|
+
return (parsed.success &&
|
|
59
|
+
parsed.data.kind === ActivityOutcomeKind.Blocked &&
|
|
60
|
+
'status' in parsed.data.data &&
|
|
61
|
+
parsed.data.data.status === 'NEEDS_CLARIFICATION');
|
|
62
|
+
}
|
|
55
63
|
export const agentActivityOutcomeKinds = [
|
|
56
64
|
ActivityOutcomeKind.Done,
|
|
57
65
|
ActivityOutcomeKind.Rejected,
|
|
@@ -124,6 +124,6 @@ export function resolveWakeVersion(options = {}) {
|
|
|
124
124
|
return `g${headHash.slice(0, 7)}`;
|
|
125
125
|
return '0.1.0-dev';
|
|
126
126
|
}
|
|
127
|
-
export const wakeVersion = "v0.4.
|
|
127
|
+
export const wakeVersion = "v0.4.40";
|
|
128
128
|
/** The registry-installable version used by packaged sandbox images. */
|
|
129
129
|
export const wakePackageVersion = packageVersion(defaultRepoRoot) ?? wakeVersion;
|
|
@@ -5,18 +5,19 @@ import { mergeBatches, reportPartialPollFailure } from './poll-watermark.js';
|
|
|
5
5
|
import { githubReviewObservation } from './review-source.js';
|
|
6
6
|
export async function reviewCommentEventsFor(context, pullRequests) {
|
|
7
7
|
if (context.client.listReviewComments === undefined)
|
|
8
|
-
return { drafts: [], succeeded: true };
|
|
8
|
+
return { drafts: [], succeeded: true, complete: true };
|
|
9
9
|
const items = await Promise.all(pullRequests.map(async (pullRequest) => {
|
|
10
10
|
try {
|
|
11
11
|
const comments = await context.client.listReviewComments(context.owner, context.repo, pullRequest.number, context.config.polling.commentPageSize, context.config.polling.maxPerRepo);
|
|
12
12
|
return {
|
|
13
13
|
succeeded: true,
|
|
14
|
+
complete: comments.length < context.config.polling.maxPerRepo,
|
|
14
15
|
drafts: (await Promise.all(comments.map((comment) => issueCommentEventsForComment(context, pullRequest, comment)))).flat(),
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
18
|
catch {
|
|
18
19
|
reportPartialPollFailure(context.repository, `pull-request review comments #${pullRequest.number}`);
|
|
19
|
-
return { drafts: [], succeeded: false };
|
|
20
|
+
return { drafts: [], succeeded: false, complete: false };
|
|
20
21
|
}
|
|
21
22
|
}));
|
|
22
23
|
return mergeBatches(items);
|
|
@@ -27,6 +28,7 @@ export async function reviewEventsFor(context, pullRequests) {
|
|
|
27
28
|
const reviews = await context.client.listReviews(context.owner, context.repo, pullRequest.number, context.config.polling.commentPageSize, context.config.polling.maxPerRepo);
|
|
28
29
|
return {
|
|
29
30
|
succeeded: true,
|
|
31
|
+
complete: reviews.length < context.config.polling.maxPerRepo,
|
|
30
32
|
drafts: reviews.flatMap((review) => githubReviewObservation({
|
|
31
33
|
repository: context.repository,
|
|
32
34
|
pullRequest,
|
|
@@ -37,7 +39,7 @@ export async function reviewEventsFor(context, pullRequests) {
|
|
|
37
39
|
}
|
|
38
40
|
catch {
|
|
39
41
|
reportPartialPollFailure(context.repository, `pull-request reviews #${pullRequest.number}`);
|
|
40
|
-
return { drafts: [], succeeded: false };
|
|
42
|
+
return { drafts: [], succeeded: false, complete: false };
|
|
41
43
|
}
|
|
42
44
|
}));
|
|
43
45
|
return mergeBatches(items);
|
|
@@ -48,12 +50,13 @@ export async function issueCommentEventsFor(context, issues, since) {
|
|
|
48
50
|
const comments = await context.client.listIssueComments(context.owner, context.repo, issue.number, context.config.polling.commentPageSize, since, context.config.polling.maxPerRepo);
|
|
49
51
|
return {
|
|
50
52
|
succeeded: true,
|
|
53
|
+
complete: comments.length < context.config.polling.maxPerRepo,
|
|
51
54
|
drafts: (await Promise.all(comments.map((comment) => issueCommentEventsForComment(context, issue, comment)))).flat(),
|
|
52
55
|
};
|
|
53
56
|
}
|
|
54
57
|
catch {
|
|
55
58
|
reportPartialPollFailure(context.repository, `issue comments #${issue.number}`);
|
|
56
|
-
return { drafts: [], succeeded: false };
|
|
59
|
+
return { drafts: [], succeeded: false, complete: false };
|
|
57
60
|
}
|
|
58
61
|
}));
|
|
59
62
|
return mergeBatches(items);
|
|
@@ -1,12 +1,36 @@
|
|
|
1
1
|
import type { CheckpointStore } from '@atolis-hq/eventing';
|
|
2
2
|
import type { AdapterId } from '../../contracts/identifiers.js';
|
|
3
3
|
import type { GitHubAdapterEventData } from '../contracts/events.js';
|
|
4
|
+
export declare const minimumPollOverlapMs: number;
|
|
4
5
|
export interface PollBatch {
|
|
5
6
|
readonly drafts: readonly GitHubAdapterEventData[];
|
|
6
7
|
readonly succeeded: boolean;
|
|
8
|
+
readonly complete: boolean;
|
|
7
9
|
}
|
|
8
10
|
export declare function mergeBatches(batches: readonly PollBatch[]): PollBatch;
|
|
9
11
|
export declare function watermarkCheckpoint(adapter: AdapterId | undefined, repository: string): string;
|
|
10
12
|
export declare function loadWatermark(checkpoints: CheckpointStore | undefined, adapter: AdapterId | undefined, owner: string, repo: string): Promise<number>;
|
|
11
13
|
export declare function overlapSince(watermark: number, overlapMs: number): string | undefined;
|
|
14
|
+
export declare function providerWatermark(previous: number, updatedAts: readonly string[]): number | undefined;
|
|
15
|
+
export declare function hasProviderTimestamp(payload: {
|
|
16
|
+
readonly updated_at: string;
|
|
17
|
+
}): boolean;
|
|
18
|
+
export declare function timestampsValid(...groups: readonly (readonly {
|
|
19
|
+
readonly updated_at: string;
|
|
20
|
+
}[])[]): boolean;
|
|
21
|
+
export declare function batchesSucceeded(batches: readonly PollBatch[]): boolean;
|
|
22
|
+
export declare function batchesComplete(batches: readonly PollBatch[]): boolean;
|
|
23
|
+
export declare function completedProviderWatermark(input: {
|
|
24
|
+
readonly previous: number;
|
|
25
|
+
readonly maximumResults: number;
|
|
26
|
+
readonly issues: readonly {
|
|
27
|
+
readonly updated_at: string;
|
|
28
|
+
}[];
|
|
29
|
+
readonly pullRequests: readonly {
|
|
30
|
+
readonly updated_at: string;
|
|
31
|
+
}[];
|
|
32
|
+
readonly topLevelSucceeded: boolean;
|
|
33
|
+
readonly topLevelTimestampsValid: boolean;
|
|
34
|
+
readonly nestedBatches: readonly PollBatch[];
|
|
35
|
+
}): number | undefined;
|
|
12
36
|
export declare function reportPartialPollFailure(repository: string, query: string): void;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
export const minimumPollOverlapMs = 5 * 60_000;
|
|
1
2
|
export function mergeBatches(batches) {
|
|
2
3
|
return {
|
|
3
4
|
drafts: batches.flatMap((batch) => batch.drafts),
|
|
4
5
|
succeeded: batches.every((batch) => batch.succeeded),
|
|
6
|
+
complete: batches.every((batch) => batch.complete),
|
|
5
7
|
};
|
|
6
8
|
}
|
|
7
9
|
export function watermarkCheckpoint(adapter, repository) {
|
|
@@ -15,7 +17,41 @@ export async function loadWatermark(checkpoints, adapter, owner, repo) {
|
|
|
15
17
|
export function overlapSince(watermark, overlapMs) {
|
|
16
18
|
if (!Number.isFinite(watermark) || watermark <= 0)
|
|
17
19
|
return undefined;
|
|
18
|
-
return new Date(Math.max(0, watermark - overlapMs)).toISOString();
|
|
20
|
+
return new Date(Math.max(0, watermark - Math.max(overlapMs, minimumPollOverlapMs))).toISOString();
|
|
21
|
+
}
|
|
22
|
+
export function providerWatermark(previous, updatedAts) {
|
|
23
|
+
const latest = updatedAts.reduce((current, updatedAt) => {
|
|
24
|
+
const timestamp = Date.parse(updatedAt);
|
|
25
|
+
if (!Number.isFinite(timestamp) || timestamp < previous)
|
|
26
|
+
return current;
|
|
27
|
+
return current === undefined || timestamp > current ? timestamp : current;
|
|
28
|
+
}, undefined);
|
|
29
|
+
return latest === undefined || latest === previous ? undefined : latest;
|
|
30
|
+
}
|
|
31
|
+
export function hasProviderTimestamp(payload) {
|
|
32
|
+
return Number.isFinite(Date.parse(payload.updated_at));
|
|
33
|
+
}
|
|
34
|
+
export function timestampsValid(...groups) {
|
|
35
|
+
return groups.flat().every(hasProviderTimestamp);
|
|
36
|
+
}
|
|
37
|
+
export function batchesSucceeded(batches) {
|
|
38
|
+
return batches.every((batch) => batch.succeeded);
|
|
39
|
+
}
|
|
40
|
+
export function batchesComplete(batches) {
|
|
41
|
+
return batches.every((batch) => batch.complete);
|
|
42
|
+
}
|
|
43
|
+
export function completedProviderWatermark(input) {
|
|
44
|
+
if (!input.topLevelSucceeded ||
|
|
45
|
+
!input.topLevelTimestampsValid ||
|
|
46
|
+
!batchesSucceeded(input.nestedBatches) ||
|
|
47
|
+
!batchesComplete(input.nestedBatches) ||
|
|
48
|
+
input.issues.length >= input.maximumResults ||
|
|
49
|
+
input.pullRequests.length >= input.maximumResults)
|
|
50
|
+
return undefined;
|
|
51
|
+
return providerWatermark(input.previous, [
|
|
52
|
+
...input.issues.map((issue) => issue.updated_at),
|
|
53
|
+
...input.pullRequests.map((pullRequest) => pullRequest.updated_at),
|
|
54
|
+
]);
|
|
19
55
|
}
|
|
20
56
|
export function reportPartialPollFailure(repository, query) {
|
|
21
57
|
process.stderr.write(`GitHub poll partial failure for ${repository}: ${query}; preserving watermark for replay\n`);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { GitHubCheckRunPayload, GitHubCommitStatusPayload } from '../contracts/payloads.js';
|
|
2
|
+
export interface CheckEvidence {
|
|
3
|
+
readonly available: boolean;
|
|
4
|
+
readonly complete: boolean;
|
|
5
|
+
readonly checkRuns: readonly GitHubCheckRunPayload[];
|
|
6
|
+
readonly statuses: readonly GitHubCommitStatusPayload[];
|
|
7
|
+
}
|
|
8
|
+
export interface ChangedFiles {
|
|
9
|
+
readonly files: readonly string[] | undefined;
|
|
10
|
+
readonly complete: boolean;
|
|
11
|
+
}
|
|
12
|
+
interface PullRequestEvidenceClient {
|
|
13
|
+
listCheckRunsForRef(owner: string, repo: string, ref: string, maxResults?: number): Promise<readonly GitHubCheckRunPayload[]>;
|
|
14
|
+
getCombinedStatusForRef(owner: string, repo: string, ref: string, maxResults?: number): Promise<readonly GitHubCommitStatusPayload[]>;
|
|
15
|
+
listPullRequestFiles(owner: string, repo: string, pullNumber: number, maxResults?: number): Promise<readonly string[]>;
|
|
16
|
+
}
|
|
17
|
+
export declare function readPullRequestEvidence(client: PullRequestEvidenceClient, owner: string, repo: string, headRevision: string, pullNumber: number, maxResults: number): Promise<readonly [CheckEvidence, ChangedFiles]>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export async function readPullRequestEvidence(client, owner, repo, headRevision, pullNumber, maxResults) {
|
|
2
|
+
return Promise.all([
|
|
3
|
+
readCheckEvidence(client, owner, repo, headRevision, maxResults),
|
|
4
|
+
readChangedFiles(client, owner, repo, pullNumber, maxResults),
|
|
5
|
+
]);
|
|
6
|
+
}
|
|
7
|
+
async function readCheckEvidence(client, owner, repo, headRevision, maxResults) {
|
|
8
|
+
try {
|
|
9
|
+
const [checkRuns, statuses] = await Promise.all([
|
|
10
|
+
client.listCheckRunsForRef(owner, repo, headRevision, maxResults),
|
|
11
|
+
client.getCombinedStatusForRef(owner, repo, headRevision, maxResults),
|
|
12
|
+
]);
|
|
13
|
+
return {
|
|
14
|
+
available: true,
|
|
15
|
+
complete: checkRuns.length < maxResults && statuses.length < maxResults,
|
|
16
|
+
checkRuns,
|
|
17
|
+
statuses,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return { available: false, complete: false, checkRuns: [], statuses: [] };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function readChangedFiles(client, owner, repo, pullNumber, maxResults) {
|
|
25
|
+
try {
|
|
26
|
+
const files = await client.listPullRequestFiles(owner, repo, pullNumber, maxResults);
|
|
27
|
+
return { files, complete: files.length < maxResults };
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return { files: undefined, complete: false };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import type { AdapterId } from '../../contracts/identifiers.js';
|
|
2
2
|
import type { ExternalEventSource } from '../application/poll-service.js';
|
|
3
3
|
import { type GitHubCheckRunPayload, type GitHubCommitStatusPayload, type GitHubPullRequestPayload } from '../contracts/payloads.js';
|
|
4
|
+
import type { PollBatch } from './poll-watermark.js';
|
|
4
5
|
export interface GitHubPullRequestSourceClient {
|
|
5
6
|
listPullRequests(owner: string, repo: string, maxResults: number): Promise<readonly GitHubPullRequestPayload[]>;
|
|
6
7
|
listCheckRunsForRef(owner: string, repo: string, ref: string, maxResults?: number): Promise<readonly GitHubCheckRunPayload[]>;
|
|
7
8
|
getCombinedStatusForRef(owner: string, repo: string, ref: string, maxResults?: number): Promise<readonly GitHubCommitStatusPayload[]>;
|
|
8
9
|
listPullRequestFiles(owner: string, repo: string, pullNumber: number, maxResults?: number): Promise<readonly string[]>;
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
+
interface GitHubPullRequestSourceInput {
|
|
11
12
|
readonly client: GitHubPullRequestSourceClient;
|
|
12
13
|
readonly repository: string;
|
|
13
14
|
readonly maxResults: number;
|
|
14
15
|
readonly maxConcurrency?: number;
|
|
15
16
|
readonly adapter?: AdapterId;
|
|
16
|
-
}
|
|
17
|
+
}
|
|
18
|
+
export declare function createGitHubPullRequestSource(input: GitHubPullRequestSourceInput): ExternalEventSource;
|
|
19
|
+
export declare function pullRequestEventsFor(input: GitHubPullRequestSourceInput, signal: AbortSignal): Promise<PollBatch>;
|
|
20
|
+
export {};
|
|
@@ -8,30 +8,39 @@ import { formatGitHubResourceKey } from '../contracts/external-key.js';
|
|
|
8
8
|
import { gitHubAssigneeLogins, gitHubLabelNames, } from '../contracts/payloads.js';
|
|
9
9
|
import { GitHubAdapter, GitHubCheckRunStatus, UnknownGitHubIdentity, UnknownGitHubRevision, } from '../contracts/vocabulary.js';
|
|
10
10
|
import { withoutWakeMarkers } from './content-fingerprint.js';
|
|
11
|
+
import { readPullRequestEvidence } from './pr-evidence.js';
|
|
11
12
|
export function createGitHubPullRequestSource(input) {
|
|
12
|
-
const { owner, repo } = parseRepository(input.repository);
|
|
13
13
|
return {
|
|
14
14
|
async poll(signal) {
|
|
15
|
-
signal.
|
|
16
|
-
const pullRequests = await input.client.listPullRequests(owner, repo, input.maxResults);
|
|
17
|
-
return mapConcurrent(pullRequests, input.maxConcurrency ?? 4, async (pullRequest) => {
|
|
18
|
-
signal.throwIfAborted();
|
|
19
|
-
const headRevision = fallback(pullRequest.head?.sha, pullRequest.updated_at);
|
|
20
|
-
const [evidence, changedFiles] = await Promise.all([
|
|
21
|
-
readCheckEvidence(input.client, owner, repo, headRevision, input.maxResults),
|
|
22
|
-
readChangedFiles(input.client, owner, repo, pullRequest.number, input.maxResults),
|
|
23
|
-
]);
|
|
24
|
-
return pullRequestObservation({
|
|
25
|
-
repository: input.repository,
|
|
26
|
-
pullRequest,
|
|
27
|
-
evidence,
|
|
28
|
-
changedFiles,
|
|
29
|
-
...(input.adapter === undefined ? {} : { adapter: input.adapter }),
|
|
30
|
-
});
|
|
31
|
-
});
|
|
15
|
+
return (await pullRequestEventsFor(input, signal)).drafts;
|
|
32
16
|
},
|
|
33
17
|
};
|
|
34
18
|
}
|
|
19
|
+
export async function pullRequestEventsFor(input, signal) {
|
|
20
|
+
const { owner, repo } = parseRepository(input.repository);
|
|
21
|
+
signal.throwIfAborted();
|
|
22
|
+
const pullRequests = await input.client.listPullRequests(owner, repo, input.maxResults);
|
|
23
|
+
const results = await mapConcurrent(pullRequests, input.maxConcurrency ?? 4, async (pullRequest) => {
|
|
24
|
+
signal.throwIfAborted();
|
|
25
|
+
const headRevision = fallback(pullRequest.head?.sha, pullRequest.updated_at);
|
|
26
|
+
const [evidence, changedFiles] = await readPullRequestEvidence(input.client, owner, repo, headRevision, pullRequest.number, input.maxResults);
|
|
27
|
+
return {
|
|
28
|
+
complete: evidence.complete && changedFiles.complete,
|
|
29
|
+
draft: pullRequestObservation({
|
|
30
|
+
repository: input.repository,
|
|
31
|
+
pullRequest,
|
|
32
|
+
evidence,
|
|
33
|
+
changedFiles: changedFiles.files,
|
|
34
|
+
...(input.adapter === undefined ? {} : { adapter: input.adapter }),
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
drafts: results.map((result) => result.draft),
|
|
40
|
+
succeeded: true,
|
|
41
|
+
complete: results.every((result) => result.complete),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
35
44
|
function pullRequestObservation(input) {
|
|
36
45
|
const pullRequest = input.pullRequest;
|
|
37
46
|
const key = formatGitHubResourceKey({
|
|
@@ -93,26 +102,6 @@ function normalizeCheckEvidence(checkRuns, statuses) {
|
|
|
93
102
|
return PullRequestCheckState.Passing;
|
|
94
103
|
return PullRequestCheckState.Unknown;
|
|
95
104
|
}
|
|
96
|
-
async function readCheckEvidence(client, owner, repo, headRevision, maxResults) {
|
|
97
|
-
try {
|
|
98
|
-
const [checkRuns, statuses] = await Promise.all([
|
|
99
|
-
client.listCheckRunsForRef(owner, repo, headRevision, maxResults),
|
|
100
|
-
client.getCombinedStatusForRef(owner, repo, headRevision, maxResults),
|
|
101
|
-
]);
|
|
102
|
-
return { available: true, checkRuns, statuses };
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
return { available: false, checkRuns: [], statuses: [] };
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
async function readChangedFiles(client, owner, repo, pullNumber, maxResults) {
|
|
109
|
-
try {
|
|
110
|
-
return await client.listPullRequestFiles(owner, repo, pullNumber, maxResults);
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
return undefined;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
105
|
async function mapConcurrent(values, concurrency, transform) {
|
|
117
106
|
const output = new Array(values.length);
|
|
118
107
|
let nextIndex = 0;
|
|
@@ -3,8 +3,8 @@ import { GitHubEventType } from '../contracts/events.js';
|
|
|
3
3
|
import { createGitHubAdapterHealthRegistry, } from './adapter-health-registry.js';
|
|
4
4
|
import { issueCommentEventsFor, reviewCommentEventsFor, reviewEventsFor, } from './comment-source.js';
|
|
5
5
|
import { issueObservation } from './issue-source.js';
|
|
6
|
-
import { loadWatermark, overlapSince, reportPartialPollFailure, watermarkCheckpoint, } from './poll-watermark.js';
|
|
7
|
-
import {
|
|
6
|
+
import { batchesComplete, batchesSucceeded, completedProviderWatermark, hasProviderTimestamp, loadWatermark, overlapSince, reportPartialPollFailure, timestampsValid, watermarkCheckpoint, } from './poll-watermark.js';
|
|
7
|
+
import { pullRequestEventsFor } from './pr-source.js';
|
|
8
8
|
import { createGitHubRequestCoordinator, } from './request-coordinator.js';
|
|
9
9
|
export function createGitHubSource(config, client, adapter, requests = createGitHubRequestCoordinator({
|
|
10
10
|
maxConcurrent: config.polling.maxConcurrent,
|
|
@@ -36,12 +36,11 @@ export function createGitHubSource(config, client, adapter, requests = createGit
|
|
|
36
36
|
owner,
|
|
37
37
|
repo,
|
|
38
38
|
watermark: await loadWatermark(state?.checkpoints, adapter, owner, repo),
|
|
39
|
-
now,
|
|
40
39
|
health,
|
|
41
40
|
})));
|
|
42
41
|
for (const result of perRepository)
|
|
43
|
-
if (result.
|
|
44
|
-
pendingWatermarks.set(result.repository, result.
|
|
42
|
+
if (result.watermark !== undefined)
|
|
43
|
+
pendingWatermarks.set(result.repository, result.watermark);
|
|
45
44
|
return perRepository
|
|
46
45
|
.flatMap((result) => result.drafts)
|
|
47
46
|
.filter((draft) => {
|
|
@@ -87,15 +86,7 @@ function limitGitHubSourceClient(client, requests) {
|
|
|
87
86
|
}
|
|
88
87
|
async function pollRepository(input) {
|
|
89
88
|
const { client, config, adapter, signal, owner, repo, health } = input;
|
|
90
|
-
const
|
|
91
|
-
const context = {
|
|
92
|
-
client,
|
|
93
|
-
config,
|
|
94
|
-
adapter,
|
|
95
|
-
owner,
|
|
96
|
-
repo,
|
|
97
|
-
repository: `${owner}/${repo}`,
|
|
98
|
-
};
|
|
89
|
+
const context = createRepositoryPollContext(client, config, adapter, owner, repo);
|
|
99
90
|
const since = overlapSince(input.watermark, config.polling.lookbackMs);
|
|
100
91
|
const [issuesResult, pullRequestsResult] = await fetchRepositoryItems({
|
|
101
92
|
client,
|
|
@@ -105,29 +96,43 @@ async function pollRepository(input) {
|
|
|
105
96
|
since,
|
|
106
97
|
});
|
|
107
98
|
reportRepositoryReadResults(context, health, issuesResult, pullRequestsResult);
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
|
|
99
|
+
const returnedIssues = isFulfilled(issuesResult) ? issuesResult.value : [];
|
|
100
|
+
const issues = returnedIssues.filter(hasProviderTimestamp);
|
|
101
|
+
const returnedPullRequestPayloads = isFulfilled(pullRequestsResult)
|
|
102
|
+
? pullRequestsResult.value
|
|
111
103
|
: [];
|
|
104
|
+
const topLevelTimestampsValid = timestampsValid(returnedIssues, returnedPullRequestPayloads);
|
|
105
|
+
const pullRequestPayloads = returnedPullRequestPayloads
|
|
106
|
+
.filter(hasProviderTimestamp)
|
|
107
|
+
.filter((pullRequest) => since === undefined || pullRequest.updated_at >= since);
|
|
112
108
|
const [pullRequests, reviews, reviewComments, issueComments] = await Promise.all([
|
|
113
|
-
|
|
109
|
+
pullRequestEventsFor({
|
|
114
110
|
client: { ...client, listPullRequests: async () => pullRequestPayloads },
|
|
115
111
|
repository: context.repository,
|
|
116
112
|
maxResults: config.polling.maxPerRepo,
|
|
117
113
|
...(adapter === undefined ? {} : { adapter }),
|
|
118
|
-
}
|
|
114
|
+
}, signal),
|
|
119
115
|
reviewEventsFor(context, pullRequestPayloads),
|
|
120
116
|
reviewCommentEventsFor(context, pullRequestPayloads),
|
|
121
117
|
issueCommentEventsFor(context, [...issues, ...pullRequestPayloads], since),
|
|
122
118
|
]);
|
|
119
|
+
const nestedBatches = [pullRequests, reviews, reviewComments, issueComments];
|
|
123
120
|
return {
|
|
124
121
|
repository: context.repository,
|
|
125
|
-
|
|
126
|
-
|
|
122
|
+
watermark: completedProviderWatermark({
|
|
123
|
+
previous: input.watermark,
|
|
124
|
+
maximumResults: config.polling.maxPerRepo,
|
|
125
|
+
issues: returnedIssues,
|
|
126
|
+
pullRequests: returnedPullRequestPayloads,
|
|
127
|
+
topLevelSucceeded: isFulfilled(issuesResult) && isFulfilled(pullRequestsResult),
|
|
128
|
+
topLevelTimestampsValid,
|
|
129
|
+
nestedBatches,
|
|
130
|
+
}),
|
|
131
|
+
succeeded: topLevelTimestampsValid &&
|
|
132
|
+
isFulfilled(issuesResult) &&
|
|
127
133
|
isFulfilled(pullRequestsResult) &&
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
issueComments.succeeded,
|
|
134
|
+
batchesComplete(nestedBatches) &&
|
|
135
|
+
batchesSucceeded(nestedBatches),
|
|
131
136
|
drafts: [
|
|
132
137
|
...issues
|
|
133
138
|
.filter((issue) => issue.pull_request === undefined)
|
|
@@ -136,13 +141,16 @@ async function pollRepository(input) {
|
|
|
136
141
|
issue,
|
|
137
142
|
...(adapter === undefined ? {} : { adapter }),
|
|
138
143
|
})),
|
|
139
|
-
...pullRequests,
|
|
144
|
+
...pullRequests.drafts,
|
|
140
145
|
...reviews.drafts,
|
|
141
146
|
...reviewComments.drafts,
|
|
142
147
|
...issueComments.drafts,
|
|
143
148
|
],
|
|
144
149
|
};
|
|
145
150
|
}
|
|
151
|
+
function createRepositoryPollContext(client, config, adapter, owner, repo) {
|
|
152
|
+
return { client, config, adapter, owner, repo, repository: `${owner}/${repo}` };
|
|
153
|
+
}
|
|
146
154
|
function fetchRepositoryItems(input) {
|
|
147
155
|
const { client, config, owner, repo, since } = input;
|
|
148
156
|
return Promise.allSettled([
|
|
@@ -31,6 +31,7 @@ export declare class AdvanceWorkflow {
|
|
|
31
31
|
restartBlockedFailedStage(id: WorkflowInstanceId, context: CommandContext): Promise<import("../index.js").WorkflowInstanceView>;
|
|
32
32
|
private retryBlockedFailedStageWith;
|
|
33
33
|
resumeBlockedStageForChanges(id: WorkflowInstanceId, context: CommandContext): Promise<import("../index.js").WorkflowInstanceView | null>;
|
|
34
|
+
resumeNeedsClarificationStage(id: WorkflowInstanceId, context: CommandContext): Promise<import("../index.js").WorkflowInstanceView | null>;
|
|
34
35
|
get(id: WorkflowInstanceId): Promise<import("../index.js").WorkflowInstanceView | null>;
|
|
35
36
|
listPendingActivations(workItemId?: string): Promise<{
|
|
36
37
|
workflow: import("../index.js").WorkflowInstanceView;
|
|
@@ -3,7 +3,7 @@ import { OrchestrationEventType } from '../contracts/events.js';
|
|
|
3
3
|
import { commandName } from '../contracts/identifiers.js';
|
|
4
4
|
import { WorkflowStatus } from '../contracts/vocabulary.js';
|
|
5
5
|
import { workflowEventData } from '../domain/decision-events.js';
|
|
6
|
-
import { requestChangesResume as decideChangesResume, requestFreshOperatorRetry as decideFreshOperatorRetry, requestOperatorRetry as decideOperatorRetry, requestSupplementalActivity as decideSupplementalActivity, requestRunnerQuotaRetry, } from '../domain/interpreter.js';
|
|
6
|
+
import { requestChangesResume as decideChangesResume, requestFreshOperatorRetry as decideFreshOperatorRetry, requestOperatorRetry as decideOperatorRetry, requestSupplementalActivity as decideSupplementalActivity, isNeedsClarificationResumeEligible, requestRunnerQuotaRetry, } from '../domain/interpreter.js';
|
|
7
7
|
import { isAuthorisedActor } from '../domain/supplemental-policy.js';
|
|
8
8
|
import { appendWithIntentRecovery } from './durable-append.js';
|
|
9
9
|
import { acceptResourceTransition, matchResourceTransitions, } from './resource-transition-matching.js';
|
|
@@ -190,6 +190,12 @@ export class AdvanceWorkflow {
|
|
|
190
190
|
return recovered.view;
|
|
191
191
|
return (await this.repository.loadRequired(id)).view;
|
|
192
192
|
}
|
|
193
|
+
async resumeNeedsClarificationStage(id, context) {
|
|
194
|
+
const loaded = await this.repository.load(id);
|
|
195
|
+
if (loaded.view === null || !isNeedsClarificationResumeEligible(loaded.view))
|
|
196
|
+
return loaded.view;
|
|
197
|
+
return this.resumeBlockedStageForChanges(id, context);
|
|
198
|
+
}
|
|
193
199
|
async get(id) {
|
|
194
200
|
return (await this.repository.load(id)).view;
|
|
195
201
|
}
|
|
@@ -41,12 +41,15 @@ export function createConversationCommandReactor(orchestration) {
|
|
|
41
41
|
occurredAt: event.occurredAt,
|
|
42
42
|
actor: { kind: EventActorKind.Integration, id: entry.origin.adapter },
|
|
43
43
|
});
|
|
44
|
-
if (conversationCommand(entry.body) !== null
|
|
44
|
+
if (conversationCommand(entry.body) !== null)
|
|
45
45
|
return;
|
|
46
46
|
for (const workflow of await orchestration.listAll()) {
|
|
47
47
|
if (workflow.workItemId !== workItemId)
|
|
48
48
|
continue;
|
|
49
|
-
|
|
49
|
+
const resume = input.authorized
|
|
50
|
+
? orchestration.resumeBlockedStageForChanges.bind(orchestration)
|
|
51
|
+
: orchestration.resumeNeedsClarificationStage.bind(orchestration);
|
|
52
|
+
await resume(workflow.workflowInstanceId, {
|
|
50
53
|
commandId: `${event.eventId}:conversation-reply`,
|
|
51
54
|
correlationId: correlationId(event.correlationId),
|
|
52
55
|
occurredAt: event.occurredAt,
|
|
@@ -61,6 +61,7 @@ export declare class OrchestrationService {
|
|
|
61
61
|
retryBlockedFailedStage(workflowInstanceId: WorkflowInstanceId, context: CommandContext): Promise<WorkflowInstanceView>;
|
|
62
62
|
restartBlockedFailedStage(workflowInstanceId: WorkflowInstanceId, context: CommandContext): Promise<WorkflowInstanceView>;
|
|
63
63
|
resumeBlockedStageForChanges(workflowInstanceId: WorkflowInstanceId, context: CommandContext): Promise<WorkflowInstanceView | null>;
|
|
64
|
+
resumeNeedsClarificationStage(workflowInstanceId: WorkflowInstanceId, context: CommandContext): Promise<WorkflowInstanceView | null>;
|
|
64
65
|
extendBlockedGroupBudget(workflowInstanceId: WorkflowInstanceId, authority: {
|
|
65
66
|
readonly kind: string;
|
|
66
67
|
}, context: CommandContext): Promise<import("../contracts/views.js").GroupBudgetExhaustedView | WorkflowInstanceView>;
|
|
@@ -137,6 +137,9 @@ export class OrchestrationService {
|
|
|
137
137
|
resumeBlockedStageForChanges(workflowInstanceId, context) {
|
|
138
138
|
return this.transitionWatchChildren(context, () => this.advanceWorkflow.resumeBlockedStageForChanges(workflowInstanceId, context));
|
|
139
139
|
}
|
|
140
|
+
resumeNeedsClarificationStage(workflowInstanceId, context) {
|
|
141
|
+
return this.transitionWatchChildren(context, () => this.advanceWorkflow.resumeNeedsClarificationStage(workflowInstanceId, context));
|
|
142
|
+
}
|
|
140
143
|
async extendBlockedGroupBudget(workflowInstanceId, authority, context) {
|
|
141
144
|
if (authority.kind !== ApprovalAuthorityKind.Human)
|
|
142
145
|
throw new GroupBudgetExtensionIneligibleError('A group budget extension requires human authority');
|
|
@@ -7,7 +7,7 @@ export { acceptSignal, waitForSignal } from './signal-policy.js';
|
|
|
7
7
|
export type { AcceptSignal } from './signal-policy.js';
|
|
8
8
|
export { requestSupplementalActivity } from './supplemental-policy.js';
|
|
9
9
|
export type { SupplementalActivityRequest } from './supplemental-policy.js';
|
|
10
|
-
export { isChangesResumeEligible, isGroupBudgetExtensionEligible, isOperatorRetryEligible, requestChangesResume, requestFreshOperatorRetry, requestOperatorRetry, selectOperatorRetryTarget, } from './operator-retry-policy.js';
|
|
10
|
+
export { isChangesResumeEligible, isGroupBudgetExtensionEligible, isNeedsClarificationResumeEligible, isOperatorRetryEligible, requestChangesResume, requestFreshOperatorRetry, requestOperatorRetry, selectOperatorRetryTarget, } from './operator-retry-policy.js';
|
|
11
11
|
export type { OperatorRetryRequest } from './operator-retry-policy.js';
|
|
12
12
|
export { isRunnerQuotaRetryEligible, requestRunnerQuotaRetry, } from './runner-quota-retry-policy.js';
|
|
13
13
|
export type { RunnerQuotaRetryRequest } from './runner-quota-retry-policy.js';
|
|
@@ -10,7 +10,7 @@ import { finishRoute } from './transition.js';
|
|
|
10
10
|
export { startInstance } from './activation-policy.js';
|
|
11
11
|
export { acceptSignal, waitForSignal } from './signal-policy.js';
|
|
12
12
|
export { requestSupplementalActivity } from './supplemental-policy.js';
|
|
13
|
-
export { isChangesResumeEligible, isGroupBudgetExtensionEligible, isOperatorRetryEligible, requestChangesResume, requestFreshOperatorRetry, requestOperatorRetry, selectOperatorRetryTarget, } from './operator-retry-policy.js';
|
|
13
|
+
export { isChangesResumeEligible, isGroupBudgetExtensionEligible, isNeedsClarificationResumeEligible, isOperatorRetryEligible, requestChangesResume, requestFreshOperatorRetry, requestOperatorRetry, selectOperatorRetryTarget, } from './operator-retry-policy.js';
|
|
14
14
|
export { isRunnerQuotaRetryEligible, requestRunnerQuotaRetry, } from './runner-quota-retry-policy.js';
|
|
15
15
|
export function acceptActivityOutcome(definition, state, input) {
|
|
16
16
|
if (!isPendingOutcome(state, input))
|
|
@@ -7,6 +7,8 @@ export interface OperatorRetryRequest extends DecisionContext {
|
|
|
7
7
|
export declare function isOperatorRetryEligible(view: WorkflowInstanceView): boolean;
|
|
8
8
|
export declare function selectOperatorRetryTarget(workflows: readonly WorkflowInstanceView[]): WorkflowInstanceView | undefined;
|
|
9
9
|
export declare function isChangesResumeEligible(view: WorkflowInstanceView): boolean;
|
|
10
|
+
/** A clarification reply may be supplied by any conversation participant. */
|
|
11
|
+
export declare function isNeedsClarificationResumeEligible(view: WorkflowInstanceView): boolean;
|
|
10
12
|
export declare function isGroupBudgetExtensionEligible(view: WorkflowInstanceView): boolean;
|
|
11
13
|
export declare function requestOperatorRetry(definition: CompiledWorkflow, state: WorkflowInstanceView, input: OperatorRetryRequest): OrchestrationDecision;
|
|
12
14
|
/** Re-requests the failed stage while requiring a new runner session. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ActivityOutcomeKind, BuiltInActivityName } from '../../activities/index.js';
|
|
1
|
+
import { ActivityOutcomeKind, BuiltInActivityName, isNeedsClarificationAgentOutcome, } from '../../activities/index.js';
|
|
2
2
|
import { OrchestrationEventType, WatchGateVerdictSignal } from '../contracts/events.js';
|
|
3
3
|
import { signalName, stageName, watchId } from '../contracts/identifiers.js';
|
|
4
4
|
import { ActivityActivationStatus, ApprovalAuthorityKind, WorkflowStatus, } from '../contracts/vocabulary.js';
|
|
@@ -49,20 +49,26 @@ export function selectOperatorRetryTarget(workflows) {
|
|
|
49
49
|
isOperatorRetryEligible(workflow));
|
|
50
50
|
}
|
|
51
51
|
export function isChangesResumeEligible(view) {
|
|
52
|
-
|
|
53
|
-
const completedAgentStage = pending !== undefined &&
|
|
54
|
-
pending.activity === BuiltInActivityName.Agent &&
|
|
55
|
-
pending.status === ActivityActivationStatus.Completed &&
|
|
56
|
-
pending.supplemental !== true &&
|
|
57
|
-
pending.followOnIndex === undefined &&
|
|
58
|
-
view.acceptedOutcomes.includes(pending.activationId);
|
|
59
|
-
if (!completedAgentStage)
|
|
52
|
+
if (!isCompletedAgentStage(view))
|
|
60
53
|
return false;
|
|
61
54
|
return ((view.status === WorkflowStatus.Blocked &&
|
|
62
55
|
view.blockReason === 'unconfigured outcome blocked' &&
|
|
63
56
|
view.lastOutcome?.kind === ActivityOutcomeKind.Blocked) ||
|
|
64
57
|
(view.status === WorkflowStatus.Waiting && view.waitingFor !== undefined));
|
|
65
58
|
}
|
|
59
|
+
/** A clarification reply may be supplied by any conversation participant. */
|
|
60
|
+
export function isNeedsClarificationResumeEligible(view) {
|
|
61
|
+
return isChangesResumeEligible(view) && isNeedsClarificationAgentOutcome(view.lastOutcome);
|
|
62
|
+
}
|
|
63
|
+
function isCompletedAgentStage(view) {
|
|
64
|
+
const pending = view.pendingActivation;
|
|
65
|
+
return (pending !== undefined &&
|
|
66
|
+
pending.activity === BuiltInActivityName.Agent &&
|
|
67
|
+
pending.status === ActivityActivationStatus.Completed &&
|
|
68
|
+
pending.supplemental !== true &&
|
|
69
|
+
pending.followOnIndex === undefined &&
|
|
70
|
+
view.acceptedOutcomes.includes(pending.activationId));
|
|
71
|
+
}
|
|
66
72
|
export function isGroupBudgetExtensionEligible(view) {
|
|
67
73
|
return (view.status === WorkflowStatus.Blocked &&
|
|
68
74
|
view.blockReason?.startsWith('watch group budget exhausted for ') === true &&
|