@vercel/factory 0.0.15 → 0.0.16
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/CHANGELOG.md +356 -0
- package/README.md +49 -261
- package/dist/agent-routes.d.mts +47 -3
- package/dist/agent-routes.mjs +28 -1
- package/dist/agent-routes.mjs.map +1 -1
- package/dist/api-contracts.d.mts +20 -1
- package/dist/api-contracts.mjs +2 -1
- package/dist/api-contracts.mjs.map +1 -1
- package/dist/api.d.mts +45 -2
- package/dist/api.mjs +199 -10
- package/dist/api.mjs.map +1 -1
- package/dist/approval-contracts.d.mts +6 -0
- package/dist/blob/index.d.mts +51 -14
- package/dist/blob/index.mjs +26 -10
- package/dist/blob/index.mjs.map +1 -1
- package/dist/budget.d.mts +7 -0
- package/dist/budget.mjs +6 -0
- package/dist/budget.mjs.map +1 -1
- package/dist/build-factory.d.mts +1 -0
- package/dist/change-verification/dispatch.d.mts +16 -3
- package/dist/change-verification/dispatch.mjs +45 -7
- package/dist/change-verification/dispatch.mjs.map +1 -1
- package/dist/change-verification/eve-tool.d.mts +2 -1
- package/dist/change-verification/eve-tool.mjs +35 -47
- package/dist/change-verification/eve-tool.mjs.map +1 -1
- package/dist/change-verification/result.mjs +130 -0
- package/dist/change-verification/result.mjs.map +1 -0
- package/dist/changes/eve-record-change.d.mts +2 -2
- package/dist/changes/eve-record-change.mjs +43 -9
- package/dist/changes/eve-record-change.mjs.map +1 -1
- package/dist/changes.d.mts +4 -3
- package/dist/changes.mjs +2 -2
- package/dist/changes.mjs.map +1 -1
- package/dist/client-events.d.mts +10 -3
- package/dist/client-events.mjs +6 -2
- package/dist/client-events.mjs.map +1 -1
- package/dist/client-stream.mjs +8 -2
- package/dist/client-stream.mjs.map +1 -1
- package/dist/client-transcript.mjs +5 -1
- package/dist/client-transcript.mjs.map +1 -1
- package/dist/client.d.mts +121 -12
- package/dist/client.mjs +117 -9
- package/dist/client.mjs.map +1 -1
- package/dist/code-review/contracts.d.mts +1 -0
- package/dist/code-review/eve-post-review.d.mts +4 -4
- package/dist/code-review/eve-post-review.mjs +29 -17
- package/dist/code-review/eve-post-review.mjs.map +1 -1
- package/dist/code-review/eve-review-comments.d.mts +13 -2
- package/dist/code-review/eve-review-comments.mjs +45 -11
- package/dist/code-review/eve-review-comments.mjs.map +1 -1
- package/dist/code-review/github-reporter.d.mts +2 -0
- package/dist/code-review/github-reporter.mjs +7 -5
- package/dist/code-review/github-reporter.mjs.map +1 -1
- package/dist/code-review.d.mts +3 -2
- package/dist/deepsec/eve-tool.mjs +3 -1
- package/dist/deepsec/eve-tool.mjs.map +1 -1
- package/dist/dispatch.d.mts +79 -8
- package/dist/dispatch.mjs +68 -9
- package/dist/dispatch.mjs.map +1 -1
- package/dist/eve/index.d.mts +70 -10
- package/dist/eve/index.mjs +93 -22
- package/dist/eve/index.mjs.map +1 -1
- package/dist/eve/invoke.mjs +24 -11
- package/dist/eve/invoke.mjs.map +1 -1
- package/dist/eve/session-client.d.mts +122 -4
- package/dist/eve/session-client.mjs +127 -13
- package/dist/eve/session-client.mjs.map +1 -1
- package/dist/eve/task-execution.d.mts +380 -0
- package/dist/eve/task-execution.mjs +57 -2
- package/dist/eve/task-execution.mjs.map +1 -1
- package/dist/eve/task-session.d.mts +44 -2
- package/dist/eve/task-session.mjs +44 -2
- package/dist/eve/task-session.mjs.map +1 -1
- package/dist/eve/transcript.mjs +5 -1
- package/dist/eve/transcript.mjs.map +1 -1
- package/dist/execution.d.mts +117 -9
- package/dist/execution.mjs +76 -6
- package/dist/execution.mjs.map +1 -1
- package/dist/finding-remediation/admission.d.mts +2 -0
- package/dist/finding-remediation/admission.mjs +4 -1
- package/dist/finding-remediation/admission.mjs.map +1 -1
- package/dist/findings.d.mts +1 -0
- package/dist/github-publication.d.mts +1 -0
- package/dist/github-publication.mjs +97 -84
- package/dist/github-publication.mjs.map +1 -1
- package/dist/github-transfer.d.mts +15 -6
- package/dist/github-transfer.mjs +214 -66
- package/dist/github-transfer.mjs.map +1 -1
- package/dist/github.d.mts +51 -11
- package/dist/github.mjs +126 -24
- package/dist/github.mjs.map +1 -1
- package/dist/inbox-activity.d.mts +53 -0
- package/dist/inbox-activity.mjs +41 -0
- package/dist/inbox-activity.mjs.map +1 -0
- package/dist/index.d.mts +3 -1
- package/dist/index.mjs +3 -2
- package/dist/intake-contracts.d.mts +0 -1
- package/dist/integrations/deepsec.d.mts +1 -0
- package/dist/integrations/github.d.mts +2 -2
- package/dist/integrations/github.mjs +2 -2
- package/dist/integrations/slack.d.mts +3 -1
- package/dist/integrations/slack.mjs +3 -1
- package/dist/integrations/vercel.d.mts +4 -2
- package/dist/integrations/vercel.mjs +3 -2
- package/dist/merge-resolution/eve-tools.d.mts +1 -0
- package/dist/merge-resolution/eve-tools.mjs +7 -2
- package/dist/merge-resolution/eve-tools.mjs.map +1 -1
- package/dist/model-settings.d.mts +41 -0
- package/dist/model-settings.mjs +35 -0
- package/dist/model-settings.mjs.map +1 -0
- package/dist/planning/reconcile.mjs +6 -0
- package/dist/planning/reconcile.mjs.map +1 -1
- package/dist/postgres/index.d.mts +43 -2
- package/dist/postgres/index.mjs +40 -2
- package/dist/postgres/index.mjs.map +1 -1
- package/dist/presets/software-development/dispatch.d.mts +4 -1
- package/dist/presets/software-development/dispatch.mjs +2 -1
- package/dist/presets/software-development/dispatch.mjs.map +1 -1
- package/dist/presets/software-development/task-communication.d.mts +1 -0
- package/dist/presets/software-development/task-communication.mjs +48 -11
- package/dist/presets/software-development/task-communication.mjs.map +1 -1
- package/dist/presets/software-development.d.mts +1 -0
- package/dist/pull-requests/github-publisher.d.mts +15 -1
- package/dist/pull-requests/github-publisher.mjs +61 -1
- package/dist/pull-requests/github-publisher.mjs.map +1 -1
- package/dist/pull-requests.d.mts +1 -0
- package/dist/sandbox/index.d.mts +1 -0
- package/dist/schema/agent-route.d.mts +19 -1
- package/dist/schema/agent-route.mjs +19 -1
- package/dist/schema/agent-route.mjs.map +1 -1
- package/dist/schema/factory-config.d.mts +27 -0
- package/dist/schema/factory-config.mjs +33 -3
- package/dist/schema/factory-config.mjs.map +1 -1
- package/dist/schema/repository.d.mts +4 -0
- package/dist/schema/repository.mjs +5 -1
- package/dist/schema/repository.mjs.map +1 -1
- package/dist/schema/session.d.mts +1 -0
- package/dist/schema/session.mjs +1 -0
- package/dist/schema/session.mjs.map +1 -1
- package/dist/schema/slack-pr-notifications.d.mts +12 -0
- package/dist/schema/slack-pr-notifications.mjs +11 -0
- package/dist/schema/slack-pr-notifications.mjs.map +1 -0
- package/dist/schema/task-graph.d.mts +39 -0
- package/dist/schema/task.d.mts +1 -0
- package/dist/schema/task.mjs +2 -1
- package/dist/schema/task.mjs.map +1 -1
- package/dist/schema/transcript.d.mts +6 -0
- package/dist/schema/transcript.mjs +2 -1
- package/dist/schema/transcript.mjs.map +1 -1
- package/dist/schema/work.d.mts +52 -3
- package/dist/schema/work.mjs.map +1 -1
- package/dist/session-previews.d.mts +76 -0
- package/dist/session-previews.mjs +55 -0
- package/dist/session-previews.mjs.map +1 -0
- package/dist/session-review.d.mts +120 -0
- package/dist/session-review.mjs +79 -0
- package/dist/session-review.mjs.map +1 -0
- package/dist/signal-triage.mjs +1 -1
- package/dist/signals.d.mts +1 -0
- package/dist/stall.d.mts +4 -1
- package/dist/stall.mjs +6 -2
- package/dist/stall.mjs.map +1 -1
- package/dist/store/driver.d.mts +1 -1
- package/dist/store/driver.mjs.map +1 -1
- package/dist/store/engine.d.mts +206 -8
- package/dist/store/engine.mjs +147 -13
- package/dist/store/engine.mjs.map +1 -1
- package/dist/store/memory.d.mts +18 -1
- package/dist/store/memory.mjs +18 -1
- package/dist/store/memory.mjs.map +1 -1
- package/dist/store/slack-pr-notifications.d.mts +44 -0
- package/dist/store/slack-pr-notifications.mjs +121 -0
- package/dist/store/slack-pr-notifications.mjs.map +1 -0
- package/dist/store/task-work.d.mts +121 -6
- package/dist/store/task-work.mjs +7 -4
- package/dist/store/task-work.mjs.map +1 -1
- package/dist/sweep.d.mts +28 -6
- package/dist/sweep.mjs +34 -6
- package/dist/sweep.mjs.map +1 -1
- package/dist/task-graph-view.d.mts +3 -0
- package/dist/tasks.d.mts +3 -3
- package/dist/tasks.mjs +3 -3
- package/dist/vercel-git.d.mts +35 -3
- package/dist/vercel-git.mjs +265 -33
- package/dist/vercel-git.mjs.map +1 -1
- package/dist/vercel-github-api.d.mts +103 -0
- package/dist/vercel-github-api.mjs +363 -0
- package/dist/vercel-github-api.mjs.map +1 -0
- package/dist/vercel.d.mts +3 -2
- package/dist/vercel.mjs +3 -2
- package/dist/vercel.mjs.map +1 -1
- package/dist/work-triage.d.mts +1 -0
- package/dist/workflows.d.mts +102 -4
- package/dist/workflows.mjs +55 -2
- package/dist/workflows.mjs.map +1 -1
- package/dist/workspace-files-git.d.mts +15 -0
- package/dist/workspace-files-git.mjs +61 -0
- package/dist/workspace-files-git.mjs.map +1 -0
- package/dist/workspace-files.d.mts +107 -0
- package/dist/workspace-files.mjs +74 -0
- package/dist/workspace-files.mjs.map +1 -0
- package/docs/getting-started.md +104 -0
- package/docs/index.md +100 -0
- package/docs/recipes/cancellation.md +215 -0
- package/docs/recipes/custom-workflow.md +153 -0
- package/docs/recipes/dependent-tasks.md +207 -0
- package/docs/recipes/eve-agent.md +277 -0
- package/docs/recipes/human-input.md +204 -0
- package/docs/recipes/persistence-recovery.md +268 -0
- package/docs/recipes/retry-recovery.md +241 -0
- package/docs/recipes/task-messaging.md +215 -0
- package/docs/recipes/typed-eve-result.md +161 -0
- package/docs/runtime-integration.md +137 -0
- package/package.json +17 -6
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { mintGitHubAppInstallationToken } from "../github-app.mjs";
|
|
2
2
|
import { GitHubCheckoutConflictError, GitHubPublicationApiError, checkoutGitHubRepository, createGitHubPublicationClient, factorySessionBranch, factoryTaskBranch, isSafeGitRef, readCommittedGitChange, runSandboxGit } from "../github-publication.mjs";
|
|
3
|
-
import { GitHubReviewCommentAuthorError, StalePullRequestHeadError, addLabel, answerReviewComment, closePullRequest, configuredEventRepositorySlug, createCheckRun, createPullRequest, createPullRequestComment, hasGitHubRepositoryWriteAccess, listCheckRunsForRef, listConflictedPullRequests, listOpenPullRequests, listPullRequestComments, normalizeFactoryQuestionReply, normalizeFactoryReviewComment, normalizeGitHubDefaultBranchPush, normalizeGitHubIssueOpened, normalizeGitHubPullRequest, normalizeGitHubPullRequestClosed, normalizeGitHubWorkflowRunFailure, readIssueComment, readPullRequest, readPullRequestHead, readReviewComment, removeLabel, stripFactoryQuestionMention, submitPullRequestReview, updateCheckRun, updatePullRequestComment, verifyGitHubSignature } from "../github.mjs";
|
|
3
|
+
import { GitHubReviewCommentAuthorError, StalePullRequestHeadError, addLabel, answerReviewComment, closePullRequest, configuredEventRepositorySlug, createCheckRun, createPullRequest, createPullRequestComment, ensureGitHubLabel, hasGitHubRepositoryWriteAccess, listCheckRunsForRef, listConflictedPullRequests, listOpenPullRequests, listPullRequestComments, listPullRequestDiscussion, normalizeFactoryQuestionReply, normalizeFactoryReviewComment, normalizeGitHubDefaultBranchPush, normalizeGitHubIssueOpened, normalizeGitHubPullRequest, normalizeGitHubPullRequestClosed, normalizeGitHubWorkflowRunFailure, readIssueComment, readPullRequest, readPullRequestHead, readReviewComment, removeLabel, stripFactoryQuestionMention, submitPullRequestReview, updateCheckRun, updatePullRequestComment, verifyGitHubSignature } from "../github.mjs";
|
|
4
4
|
import { createGitHubCommunicationAdapter, parseGitHubReplyAddress, renderGitHubCommunicationEvent } from "../connectors/github.mjs";
|
|
5
5
|
import { createGitHubReviewReporter } from "../code-review/github-reporter.mjs";
|
|
6
6
|
import { createGitHubPullRequestPublicationGate, createGitHubPullRequestPublisher } from "../pull-requests/github-publisher.mjs";
|
|
7
7
|
import { createGitHubPullRequestLabelManager } from "../pull-requests/github-label-manager.mjs";
|
|
8
8
|
import { createGitHubOpenPullRequestLister } from "../work-triage/github.mjs";
|
|
9
|
-
export { GitHubCheckoutConflictError, GitHubPublicationApiError, GitHubReviewCommentAuthorError, StalePullRequestHeadError, addLabel, answerReviewComment, checkoutGitHubRepository, closePullRequest, configuredEventRepositorySlug, createCheckRun, createGitHubCommunicationAdapter, createGitHubOpenPullRequestLister, createGitHubPublicationClient, createGitHubPullRequestLabelManager, createGitHubPullRequestPublicationGate, createGitHubPullRequestPublisher, createGitHubReviewReporter, createPullRequest, createPullRequestComment, factorySessionBranch, factoryTaskBranch, hasGitHubRepositoryWriteAccess, isSafeGitRef, listCheckRunsForRef, listConflictedPullRequests, listOpenPullRequests, listPullRequestComments, mintGitHubAppInstallationToken, normalizeFactoryQuestionReply, normalizeFactoryReviewComment, normalizeGitHubDefaultBranchPush, normalizeGitHubIssueOpened, normalizeGitHubPullRequest, normalizeGitHubPullRequestClosed, normalizeGitHubWorkflowRunFailure, parseGitHubReplyAddress, readCommittedGitChange, readIssueComment, readPullRequest, readPullRequestHead, readReviewComment, removeLabel, renderGitHubCommunicationEvent, runSandboxGit, stripFactoryQuestionMention, submitPullRequestReview, updateCheckRun, updatePullRequestComment, verifyGitHubSignature };
|
|
9
|
+
export { GitHubCheckoutConflictError, GitHubPublicationApiError, GitHubReviewCommentAuthorError, StalePullRequestHeadError, addLabel, answerReviewComment, checkoutGitHubRepository, closePullRequest, configuredEventRepositorySlug, createCheckRun, createGitHubCommunicationAdapter, createGitHubOpenPullRequestLister, createGitHubPublicationClient, createGitHubPullRequestLabelManager, createGitHubPullRequestPublicationGate, createGitHubPullRequestPublisher, createGitHubReviewReporter, createPullRequest, createPullRequestComment, ensureGitHubLabel, factorySessionBranch, factoryTaskBranch, hasGitHubRepositoryWriteAccess, isSafeGitRef, listCheckRunsForRef, listConflictedPullRequests, listOpenPullRequests, listPullRequestComments, listPullRequestDiscussion, mintGitHubAppInstallationToken, normalizeFactoryQuestionReply, normalizeFactoryReviewComment, normalizeGitHubDefaultBranchPush, normalizeGitHubIssueOpened, normalizeGitHubPullRequest, normalizeGitHubPullRequestClosed, normalizeGitHubWorkflowRunFailure, parseGitHubReplyAddress, readCommittedGitChange, readIssueComment, readPullRequest, readPullRequestHead, readReviewComment, removeLabel, renderGitHubCommunicationEvent, runSandboxGit, stripFactoryQuestionMention, submitPullRequestReview, updateCheckRun, updatePullRequestComment, verifyGitHubSignature };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import { SlackApprovals, SlackApprovalsOptions, SlackApprovalsStores, createSlackApprovals } from "../connectors/slack-approvals.mjs";
|
|
2
2
|
import { SlackCommunicationAdapter, SlackCommunicationAdapterOptions, SlackCommunicationDelivery, SlackMessageEvent, SlackMessageSignal, SlackReplyTarget, createSlackCommunicationAdapter, normalizeSlackMessage, parseSlackReplyAddress, renderSlackCommunicationEvent } from "../connectors/slack.mjs";
|
|
3
|
-
|
|
3
|
+
import { SlackPrNotifications, slackPrNotificationsSchema } from "../schema/slack-pr-notifications.mjs";
|
|
4
|
+
import { SlackPrNotificationSnapshot, SlackPrNotificationStore, SlackPrNotificationStoreOptions, UpdateSlackPrNotificationsInput, createSlackPrNotificationStore } from "../store/slack-pr-notifications.mjs";
|
|
5
|
+
export { type SlackApprovals, type SlackApprovalsOptions, type SlackApprovalsStores, type SlackCommunicationAdapter, type SlackCommunicationAdapterOptions, type SlackCommunicationDelivery, type SlackMessageEvent, type SlackMessageSignal, type SlackPrNotificationSnapshot, type SlackPrNotificationStore, type SlackPrNotificationStoreOptions, type SlackPrNotifications, type SlackReplyTarget, type UpdateSlackPrNotificationsInput, createSlackApprovals, createSlackCommunicationAdapter, createSlackPrNotificationStore, normalizeSlackMessage, parseSlackReplyAddress, renderSlackCommunicationEvent, slackPrNotificationsSchema };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { slackPrNotificationsSchema } from "../schema/slack-pr-notifications.mjs";
|
|
1
2
|
import { createSlackApprovals } from "../connectors/slack-approvals.mjs";
|
|
2
3
|
import { createSlackCommunicationAdapter, normalizeSlackMessage, parseSlackReplyAddress, renderSlackCommunicationEvent } from "../connectors/slack.mjs";
|
|
3
|
-
|
|
4
|
+
import { createSlackPrNotificationStore } from "../store/slack-pr-notifications.mjs";
|
|
5
|
+
export { createSlackApprovals, createSlackCommunicationAdapter, createSlackPrNotificationStore, normalizeSlackMessage, parseSlackReplyAddress, renderSlackCommunicationEvent, slackPrNotificationsSchema };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { VercelManagedGitHubApi, VercelManagedGitHubApiAction, VercelManagedGitHubApiError, VercelManagedGitHubApiOptions, VercelManagedGitHubApiSandboxCreateInput, VercelManagedGitHubIssue, VercelManagedGitHubIssueInput, VercelManagedGitHubPublication, VercelManagedGitHubPublicationInput, VercelManagedGitHubPullRequest, VercelManagedGitHubPullRequestInput, createVercelManagedGitHubApi } from "../vercel-github-api.mjs";
|
|
3
|
+
import { VercelGitHubGitTransportOptions, VercelGitHubTokenInput, VercelManagedGitHubGitTransportOptions, VercelManagedGitHubSandboxCreateInput, createVercelGitHubGitTransport, createVercelManagedGitHubGitTransport } from "../vercel-git.mjs";
|
|
2
4
|
import { VercelProductionDeployment, VercelQueueSend, VercelQueueSendOptions, VercelWorkQueue, VercelWorkQueueOptions, authenticateVercelUserToken, createVercelWorkQueue, normalizeVercelProductionDeployment, verifyVercelSignature } from "../vercel.mjs";
|
|
3
|
-
export { type VercelGitHubGitTransportOptions, type VercelGitHubTokenInput, type VercelProductionDeployment, type VercelQueueSend, type VercelQueueSendOptions, type VercelWorkQueue, type VercelWorkQueueOptions, authenticateVercelUserToken, createVercelGitHubGitTransport, createVercelWorkQueue, normalizeVercelProductionDeployment, verifyVercelSignature };
|
|
5
|
+
export { type VercelGitHubGitTransportOptions, type VercelGitHubTokenInput, type VercelManagedGitHubApi, type VercelManagedGitHubApiAction, VercelManagedGitHubApiError, type VercelManagedGitHubApiOptions, type VercelManagedGitHubApiSandboxCreateInput, type VercelManagedGitHubGitTransportOptions, type VercelManagedGitHubIssue, type VercelManagedGitHubIssueInput, type VercelManagedGitHubPublication, type VercelManagedGitHubPublicationInput, type VercelManagedGitHubPullRequest, type VercelManagedGitHubPullRequestInput, type VercelManagedGitHubSandboxCreateInput, type VercelProductionDeployment, type VercelQueueSend, type VercelQueueSendOptions, type VercelWorkQueue, type VercelWorkQueueOptions, authenticateVercelUserToken, createVercelGitHubGitTransport, createVercelManagedGitHubApi, createVercelManagedGitHubGitTransport, createVercelWorkQueue, normalizeVercelProductionDeployment, verifyVercelSignature };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { VercelManagedGitHubApiError, createVercelManagedGitHubApi } from "../vercel-github-api.mjs";
|
|
2
|
+
import { createVercelGitHubGitTransport, createVercelManagedGitHubGitTransport } from "../vercel-git.mjs";
|
|
2
3
|
import { authenticateVercelUserToken, createVercelWorkQueue, normalizeVercelProductionDeployment, verifyVercelSignature } from "../vercel.mjs";
|
|
3
|
-
export { authenticateVercelUserToken, createVercelGitHubGitTransport, createVercelWorkQueue, normalizeVercelProductionDeployment, verifyVercelSignature };
|
|
4
|
+
export { VercelManagedGitHubApiError, authenticateVercelUserToken, createVercelGitHubGitTransport, createVercelManagedGitHubApi, createVercelManagedGitHubGitTransport, createVercelWorkQueue, normalizeVercelProductionDeployment, verifyVercelSignature };
|
|
@@ -29,6 +29,7 @@ interface GitHubPublishMergeResolutionResult {
|
|
|
29
29
|
readonly updated: true;
|
|
30
30
|
readonly prNumber: number;
|
|
31
31
|
readonly prUrl: string;
|
|
32
|
+
readonly headSha: string;
|
|
32
33
|
}
|
|
33
34
|
/** Tools that reproduce and publish an exact GitHub pull-request conflict resolution. */
|
|
34
35
|
interface GitHubMergeConflictTools {
|
|
@@ -77,12 +77,16 @@ function createGitHubMergeConflictTools(stores, options) {
|
|
|
77
77
|
if ((await sandbox.run({ command: `${git} status --porcelain` })).stdout.trim() !== "") throw new Error("commit the conflict resolution before publishing");
|
|
78
78
|
if ((await sandbox.run({ command: `${git} diff --name-only --diff-filter=U` })).stdout.trim() !== "") throw new Error("merge conflicts remain unresolved");
|
|
79
79
|
const { task, conflict } = await context(taskId, ctx);
|
|
80
|
-
await pushGitHubHead({
|
|
80
|
+
const headSha = await pushGitHubHead({
|
|
81
81
|
sandbox,
|
|
82
82
|
transport: options.git,
|
|
83
83
|
repository: conflict.repository,
|
|
84
84
|
branch: conflict.headBranch,
|
|
85
85
|
expectedHead: conflict.headSha,
|
|
86
|
+
authorize: async () => {
|
|
87
|
+
const current = await context(taskId, ctx);
|
|
88
|
+
if (current.task.attempt !== task.attempt || current.task.execution?.sessionId !== task.execution?.sessionId || current.conflict.prNumber !== conflict.prNumber || current.conflict.headSha !== conflict.headSha || current.conflict.headBranch !== conflict.headBranch) throw new Error("Merge-conflict publication authority changed during preparation");
|
|
89
|
+
},
|
|
86
90
|
repositoryPath: repoPath
|
|
87
91
|
});
|
|
88
92
|
const verifying = await stores.tasks.transition(taskId, "verifying", {
|
|
@@ -94,7 +98,8 @@ function createGitHubMergeConflictTools(stores, options) {
|
|
|
94
98
|
const result = {
|
|
95
99
|
updated: true,
|
|
96
100
|
prNumber: conflict.prNumber,
|
|
97
|
-
prUrl: conflict.prUrl
|
|
101
|
+
prUrl: conflict.prUrl,
|
|
102
|
+
headSha
|
|
98
103
|
};
|
|
99
104
|
await stores.tasks.transition(taskId, "succeeded", {
|
|
100
105
|
expectFrom: "verifying",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eve-tools.mjs","names":[],"sources":["../../src/merge-resolution/eve-tools.ts"],"sourcesContent":["import { fetchGitHubRefs, pushGitHubHead, type GitHubGitTransport } from \"../github-transfer\";\nimport { z } from \"zod\";\nimport { requireTaskExecution } from \"../eve/task-execution\";\nimport { taskIdSchema, type TaskId } from \"../schema/id\";\nimport type { FactoryStores } from \"../store/engine\";\nimport { defineTool, type ToolContext, type ToolDefinition } from \"eve/tools\";\nimport { mergeConflictPayloadSchema, type MergeConflictPayload } from \"./contracts\";\n\n/** Repository, Signal, and Task capabilities required by merge-conflict tools. */\nexport type GitHubMergeConflictToolsStores = Pick<\n FactoryStores,\n \"repositories\" | \"signals\" | \"tasks\"\n>;\n\n/** Configures the credential boundary and checkout location used by merge-conflict tools. */\nexport interface GitHubMergeConflictToolsOptions {\n git: GitHubGitTransport;\n repoPath?: string;\n}\n\n/** Merge-conflict Task selected for trusted pull-request reproduction. */\nexport interface GitHubFetchMergeConflictInput {\n taskId: TaskId;\n}\n\n/** Exact pull-request conflict context and files reproduced in the sandbox. */\nexport type GitHubFetchMergeConflictResult = Readonly<MergeConflictPayload> & {\n readonly conflictedFiles: readonly string[];\n};\n\n/** Tested conflict resolution and summary supplied for trusted publication. */\nexport interface GitHubPublishMergeResolutionInput {\n taskId: TaskId;\n summary: string;\n}\n\n/** Existing pull request updated after a conflict resolution is published. */\nexport interface GitHubPublishMergeResolutionResult {\n readonly updated: true;\n readonly prNumber: number;\n readonly prUrl: string;\n}\n\n/** Tools that reproduce and publish an exact GitHub pull-request conflict resolution. */\nexport interface GitHubMergeConflictTools {\n readonly fetch_merge_conflict: ToolDefinition<\n GitHubFetchMergeConflictInput,\n GitHubFetchMergeConflictResult\n > & {\n execute(\n input: GitHubFetchMergeConflictInput,\n ctx: ToolContext,\n ): Promise<GitHubFetchMergeConflictResult>;\n };\n readonly publish_merge_resolution: ToolDefinition<\n GitHubPublishMergeResolutionInput,\n GitHubPublishMergeResolutionResult\n > & {\n execute(\n input: GitHubPublishMergeResolutionInput,\n ctx: ToolContext,\n ): Promise<GitHubPublishMergeResolutionResult>;\n };\n}\n\n/** Create tools that reproduce an exact pull-request conflict and publish its tested resolution. */\nexport function createGitHubMergeConflictTools(\n stores: GitHubMergeConflictToolsStores,\n options: GitHubMergeConflictToolsOptions,\n): GitHubMergeConflictTools {\n const repoPath = options.repoPath ?? \"repo\";\n\n async function context(\n taskId: z.infer<typeof taskIdSchema>,\n ctx: Parameters<typeof requireTaskExecution>[1],\n ) {\n const task = await requireTaskExecution(stores, ctx, taskId);\n if (task.kind !== \"merge_conflict\")\n throw new Error(`task ${taskId} is ${task.kind}, not merge_conflict`);\n if (task.state !== \"running\") throw new Error(`task ${taskId} is ${task.state}, not running`);\n if (task.origin.signalId === undefined)\n throw new Error(`task ${taskId} has no conflict signal`);\n const signal = await stores.signals.get(task.origin.signalId);\n if (\n signal === null ||\n signal.source !== \"github_merge_conflict\" ||\n signal.outcome !== \"accepted\"\n ) {\n throw new Error(`task ${taskId} has no GitHub merge conflict signal`);\n }\n if (\n task.replyTo.channel !== signal.replyTo.channel ||\n task.replyTo.address !== signal.replyTo.address\n ) {\n throw new Error(`task ${taskId} reply destination does not match its conflict signal`);\n }\n if (task.repositoryIds.length !== 1 || task.repositoryIds[0] !== signal.repositoryId) {\n throw new Error(`task ${taskId} repository does not match its conflict signal`);\n }\n const repository = await stores.repositories.get(signal.repositoryId);\n const conflict = mergeConflictPayloadSchema.parse(signal.payload);\n if (!repository?.enabled || repository.slug !== conflict.repository.toLowerCase()) {\n throw new Error(`task ${taskId} conflict repository is not enabled or does not match`);\n }\n return { task, conflict: { ...conflict, repository: repository.slug } };\n }\n\n return {\n fetch_merge_conflict: defineTool({\n description:\n \"Fetch the conflicted factory pull request and its current base, check out the PR head, and start the merge. Resolve every file returned in conflictedFiles, then test and commit the merge.\",\n inputSchema: z.object({ taskId: taskIdSchema }),\n async execute({ taskId }, ctx): Promise<GitHubFetchMergeConflictResult> {\n await context(taskId, ctx);\n const sandbox = await ctx.getSandbox();\n const { conflict } = await context(taskId, ctx);\n const git = `git -C ${repoPath}`;\n const headRef = `refs/remotes/origin/conflict-pr-${conflict.prNumber}`;\n const baseRef = `refs/remotes/origin/conflict-base-${conflict.prNumber}`;\n await fetchGitHubRefs({\n sandbox,\n transport: options.git,\n repository: conflict.repository,\n refs: [\n { remote: `refs/pull/${conflict.prNumber}/head`, local: headRef },\n { remote: `refs/heads/${conflict.baseBranch}`, local: baseRef },\n ],\n repositoryPath: repoPath,\n });\n const fetched = await sandbox.run({\n command: `${git} rev-parse ${headRef} && ${git} rev-parse ${baseRef}`,\n });\n const [headSha, baseSha] = fetched.stdout.trim().split(\"\\n\");\n if (headSha !== conflict.headSha || baseSha !== conflict.baseSha) {\n throw new Error(\n \"pull request or base changed while conflict resolution was starting; retry from a new task\",\n );\n }\n await sandbox.run({ command: `${git} checkout -B conflict-pr ${headRef}` });\n const merge = await sandbox.run({ command: `${git} merge --no-commit --no-ff ${baseRef}` });\n const conflicts = await sandbox.run({ command: `${git} diff --name-only --diff-filter=U` });\n const conflictedFiles = conflicts.stdout.trim().split(\"\\n\").filter(Boolean);\n if (conflictedFiles.length === 0) {\n throw new Error(\n typeof merge.exitCode === \"number\" && merge.exitCode !== 0\n ? `merge failed without file conflicts: ${(merge.stderr ?? \"\").trim()}`\n : \"pull request no longer has merge conflicts\",\n );\n }\n return { ...conflict, conflictedFiles };\n },\n }),\n publish_merge_resolution: defineTool({\n description:\n \"Publish a tested, committed conflict resolution to the existing pull request branch. Fails if the remote branch changed since this task started.\",\n inputSchema: z.object({\n taskId: taskIdSchema,\n summary: z.string().min(1).max(2000),\n }),\n async execute({ taskId, summary }, ctx): Promise<GitHubPublishMergeResolutionResult> {\n await context(taskId, ctx);\n const sandbox = await ctx.getSandbox();\n const git = `git -C ${repoPath}`;\n const clean = await sandbox.run({ command: `${git} status --porcelain` });\n if (clean.stdout.trim() !== \"\")\n throw new Error(\"commit the conflict resolution before publishing\");\n const unresolved = await sandbox.run({\n command: `${git} diff --name-only --diff-filter=U`,\n });\n if (unresolved.stdout.trim() !== \"\") throw new Error(\"merge conflicts remain unresolved\");\n const { task, conflict } = await context(taskId, ctx);\n await pushGitHubHead({\n sandbox,\n transport: options.git,\n repository: conflict.repository,\n branch: conflict.headBranch,\n expectedHead: conflict.headSha,\n repositoryPath: repoPath,\n });\n const verifying = await stores.tasks.transition(taskId, \"verifying\", {\n expectFrom: \"running\",\n expectAttempt: task.attempt,\n expectExecution: task.execution,\n reason: summary,\n });\n const result = {\n updated: true as const,\n prNumber: conflict.prNumber,\n prUrl: conflict.prUrl,\n };\n await stores.tasks.transition(taskId, \"succeeded\", {\n expectFrom: \"verifying\",\n expectAttempt: verifying.attempt,\n expectExecution: verifying.execution,\n reason: summary,\n output: { ...result, summary },\n });\n return result;\n },\n }),\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"eve-tools.mjs","names":[],"sources":["../../src/merge-resolution/eve-tools.ts"],"sourcesContent":["import { fetchGitHubRefs, pushGitHubHead, type GitHubGitTransport } from \"../github-transfer\";\nimport { z } from \"zod\";\nimport { requireTaskExecution } from \"../eve/task-execution\";\nimport { taskIdSchema, type TaskId } from \"../schema/id\";\nimport type { FactoryStores } from \"../store/engine\";\nimport { defineTool, type ToolContext, type ToolDefinition } from \"eve/tools\";\nimport { mergeConflictPayloadSchema, type MergeConflictPayload } from \"./contracts\";\n\n/** Repository, Signal, and Task capabilities required by merge-conflict tools. */\nexport type GitHubMergeConflictToolsStores = Pick<\n FactoryStores,\n \"repositories\" | \"signals\" | \"tasks\"\n>;\n\n/** Configures the credential boundary and checkout location used by merge-conflict tools. */\nexport interface GitHubMergeConflictToolsOptions {\n git: GitHubGitTransport;\n repoPath?: string;\n}\n\n/** Merge-conflict Task selected for trusted pull-request reproduction. */\nexport interface GitHubFetchMergeConflictInput {\n taskId: TaskId;\n}\n\n/** Exact pull-request conflict context and files reproduced in the sandbox. */\nexport type GitHubFetchMergeConflictResult = Readonly<MergeConflictPayload> & {\n readonly conflictedFiles: readonly string[];\n};\n\n/** Tested conflict resolution and summary supplied for trusted publication. */\nexport interface GitHubPublishMergeResolutionInput {\n taskId: TaskId;\n summary: string;\n}\n\n/** Existing pull request updated after a conflict resolution is published. */\nexport interface GitHubPublishMergeResolutionResult {\n readonly updated: true;\n readonly prNumber: number;\n readonly prUrl: string;\n readonly headSha: string;\n}\n\n/** Tools that reproduce and publish an exact GitHub pull-request conflict resolution. */\nexport interface GitHubMergeConflictTools {\n readonly fetch_merge_conflict: ToolDefinition<\n GitHubFetchMergeConflictInput,\n GitHubFetchMergeConflictResult\n > & {\n execute(\n input: GitHubFetchMergeConflictInput,\n ctx: ToolContext,\n ): Promise<GitHubFetchMergeConflictResult>;\n };\n readonly publish_merge_resolution: ToolDefinition<\n GitHubPublishMergeResolutionInput,\n GitHubPublishMergeResolutionResult\n > & {\n execute(\n input: GitHubPublishMergeResolutionInput,\n ctx: ToolContext,\n ): Promise<GitHubPublishMergeResolutionResult>;\n };\n}\n\n/** Create tools that reproduce an exact pull-request conflict and publish its tested resolution. */\nexport function createGitHubMergeConflictTools(\n stores: GitHubMergeConflictToolsStores,\n options: GitHubMergeConflictToolsOptions,\n): GitHubMergeConflictTools {\n const repoPath = options.repoPath ?? \"repo\";\n\n async function context(\n taskId: z.infer<typeof taskIdSchema>,\n ctx: Parameters<typeof requireTaskExecution>[1],\n ) {\n const task = await requireTaskExecution(stores, ctx, taskId);\n if (task.kind !== \"merge_conflict\")\n throw new Error(`task ${taskId} is ${task.kind}, not merge_conflict`);\n if (task.state !== \"running\") throw new Error(`task ${taskId} is ${task.state}, not running`);\n if (task.origin.signalId === undefined)\n throw new Error(`task ${taskId} has no conflict signal`);\n const signal = await stores.signals.get(task.origin.signalId);\n if (\n signal === null ||\n signal.source !== \"github_merge_conflict\" ||\n signal.outcome !== \"accepted\"\n ) {\n throw new Error(`task ${taskId} has no GitHub merge conflict signal`);\n }\n if (\n task.replyTo.channel !== signal.replyTo.channel ||\n task.replyTo.address !== signal.replyTo.address\n ) {\n throw new Error(`task ${taskId} reply destination does not match its conflict signal`);\n }\n if (task.repositoryIds.length !== 1 || task.repositoryIds[0] !== signal.repositoryId) {\n throw new Error(`task ${taskId} repository does not match its conflict signal`);\n }\n const repository = await stores.repositories.get(signal.repositoryId);\n const conflict = mergeConflictPayloadSchema.parse(signal.payload);\n if (!repository?.enabled || repository.slug !== conflict.repository.toLowerCase()) {\n throw new Error(`task ${taskId} conflict repository is not enabled or does not match`);\n }\n return { task, conflict: { ...conflict, repository: repository.slug } };\n }\n\n return {\n fetch_merge_conflict: defineTool({\n description:\n \"Fetch the conflicted factory pull request and its current base, check out the PR head, and start the merge. Resolve every file returned in conflictedFiles, then test and commit the merge.\",\n inputSchema: z.object({ taskId: taskIdSchema }),\n async execute({ taskId }, ctx): Promise<GitHubFetchMergeConflictResult> {\n await context(taskId, ctx);\n const sandbox = await ctx.getSandbox();\n const { conflict } = await context(taskId, ctx);\n const git = `git -C ${repoPath}`;\n const headRef = `refs/remotes/origin/conflict-pr-${conflict.prNumber}`;\n const baseRef = `refs/remotes/origin/conflict-base-${conflict.prNumber}`;\n await fetchGitHubRefs({\n sandbox,\n transport: options.git,\n repository: conflict.repository,\n refs: [\n { remote: `refs/pull/${conflict.prNumber}/head`, local: headRef },\n { remote: `refs/heads/${conflict.baseBranch}`, local: baseRef },\n ],\n repositoryPath: repoPath,\n });\n const fetched = await sandbox.run({\n command: `${git} rev-parse ${headRef} && ${git} rev-parse ${baseRef}`,\n });\n const [headSha, baseSha] = fetched.stdout.trim().split(\"\\n\");\n if (headSha !== conflict.headSha || baseSha !== conflict.baseSha) {\n throw new Error(\n \"pull request or base changed while conflict resolution was starting; retry from a new task\",\n );\n }\n await sandbox.run({ command: `${git} checkout -B conflict-pr ${headRef}` });\n const merge = await sandbox.run({ command: `${git} merge --no-commit --no-ff ${baseRef}` });\n const conflicts = await sandbox.run({ command: `${git} diff --name-only --diff-filter=U` });\n const conflictedFiles = conflicts.stdout.trim().split(\"\\n\").filter(Boolean);\n if (conflictedFiles.length === 0) {\n throw new Error(\n typeof merge.exitCode === \"number\" && merge.exitCode !== 0\n ? `merge failed without file conflicts: ${(merge.stderr ?? \"\").trim()}`\n : \"pull request no longer has merge conflicts\",\n );\n }\n return { ...conflict, conflictedFiles };\n },\n }),\n publish_merge_resolution: defineTool({\n description:\n \"Publish a tested, committed conflict resolution to the existing pull request branch. Fails if the remote branch changed since this task started.\",\n inputSchema: z.object({\n taskId: taskIdSchema,\n summary: z.string().min(1).max(2000),\n }),\n async execute({ taskId, summary }, ctx): Promise<GitHubPublishMergeResolutionResult> {\n await context(taskId, ctx);\n const sandbox = await ctx.getSandbox();\n const git = `git -C ${repoPath}`;\n const clean = await sandbox.run({ command: `${git} status --porcelain` });\n if (clean.stdout.trim() !== \"\")\n throw new Error(\"commit the conflict resolution before publishing\");\n const unresolved = await sandbox.run({\n command: `${git} diff --name-only --diff-filter=U`,\n });\n if (unresolved.stdout.trim() !== \"\") throw new Error(\"merge conflicts remain unresolved\");\n const { task, conflict } = await context(taskId, ctx);\n const headSha = await pushGitHubHead({\n sandbox,\n transport: options.git,\n repository: conflict.repository,\n branch: conflict.headBranch,\n expectedHead: conflict.headSha,\n authorize: async () => {\n const current = await context(taskId, ctx);\n if (\n current.task.attempt !== task.attempt ||\n current.task.execution?.sessionId !== task.execution?.sessionId ||\n current.conflict.prNumber !== conflict.prNumber ||\n current.conflict.headSha !== conflict.headSha ||\n current.conflict.headBranch !== conflict.headBranch\n ) {\n throw new Error(\"Merge-conflict publication authority changed during preparation\");\n }\n },\n repositoryPath: repoPath,\n });\n const verifying = await stores.tasks.transition(taskId, \"verifying\", {\n expectFrom: \"running\",\n expectAttempt: task.attempt,\n expectExecution: task.execution,\n reason: summary,\n });\n const result = {\n updated: true as const,\n prNumber: conflict.prNumber,\n prUrl: conflict.prUrl,\n headSha,\n };\n await stores.tasks.transition(taskId, \"succeeded\", {\n expectFrom: \"verifying\",\n expectAttempt: verifying.attempt,\n expectExecution: verifying.execution,\n reason: summary,\n output: { ...result, summary },\n });\n return result;\n },\n }),\n };\n}\n"],"mappings":";;;;;;;;AAmEA,SAAgB,+BACd,QACA,SAC0B;CAC1B,MAAM,WAAW,QAAQ,YAAY;CAErC,eAAe,QACb,QACA,KACA;EACA,MAAM,OAAO,MAAM,qBAAqB,QAAQ,KAAK,MAAM;EAC3D,IAAI,KAAK,SAAS,kBAChB,MAAM,IAAI,MAAM,QAAQ,OAAO,MAAM,KAAK,KAAK,qBAAqB;EACtE,IAAI,KAAK,UAAU,WAAW,MAAM,IAAI,MAAM,QAAQ,OAAO,MAAM,KAAK,MAAM,cAAc;EAC5F,IAAI,KAAK,OAAO,aAAa,KAAA,GAC3B,MAAM,IAAI,MAAM,QAAQ,OAAO,wBAAwB;EACzD,MAAM,SAAS,MAAM,OAAO,QAAQ,IAAI,KAAK,OAAO,QAAQ;EAC5D,IACE,WAAW,QACX,OAAO,WAAW,2BAClB,OAAO,YAAY,YAEnB,MAAM,IAAI,MAAM,QAAQ,OAAO,qCAAqC;EAEtE,IACE,KAAK,QAAQ,YAAY,OAAO,QAAQ,WACxC,KAAK,QAAQ,YAAY,OAAO,QAAQ,SAExC,MAAM,IAAI,MAAM,QAAQ,OAAO,sDAAsD;EAEvF,IAAI,KAAK,cAAc,WAAW,KAAK,KAAK,cAAc,OAAO,OAAO,cACtE,MAAM,IAAI,MAAM,QAAQ,OAAO,+CAA+C;EAEhF,MAAM,aAAa,MAAM,OAAO,aAAa,IAAI,OAAO,YAAY;EACpE,MAAM,WAAW,2BAA2B,MAAM,OAAO,OAAO;EAChE,IAAI,CAAC,YAAY,WAAW,WAAW,SAAS,SAAS,WAAW,YAAY,GAC9E,MAAM,IAAI,MAAM,QAAQ,OAAO,sDAAsD;EAEvF,OAAO;GAAE;GAAM,UAAU;IAAE,GAAG;IAAU,YAAY,WAAW;GAAK;EAAE;CACxE;CAEA,OAAO;EACL,sBAAsB,WAAW;GAC/B,aACE;GACF,aAAa,EAAE,OAAO,EAAE,QAAQ,aAAa,CAAC;GAC9C,MAAM,QAAQ,EAAE,UAAU,KAA8C;IACtE,MAAM,QAAQ,QAAQ,GAAG;IACzB,MAAM,UAAU,MAAM,IAAI,WAAW;IACrC,MAAM,EAAE,aAAa,MAAM,QAAQ,QAAQ,GAAG;IAC9C,MAAM,MAAM,UAAU;IACtB,MAAM,UAAU,mCAAmC,SAAS;IAC5D,MAAM,UAAU,qCAAqC,SAAS;IAC9D,MAAM,gBAAgB;KACpB;KACA,WAAW,QAAQ;KACnB,YAAY,SAAS;KACrB,MAAM,CACJ;MAAE,QAAQ,aAAa,SAAS,SAAS;MAAQ,OAAO;KAAQ,GAChE;MAAE,QAAQ,cAAc,SAAS;MAAc,OAAO;KAAQ,CAChE;KACA,gBAAgB;IAClB,CAAC;IAID,MAAM,CAAC,SAAS,YAAW,MAHL,QAAQ,IAAI,EAChC,SAAS,GAAG,IAAI,aAAa,QAAQ,MAAM,IAAI,aAAa,UAC9D,CAAC,EAAA,CACkC,OAAO,KAAK,CAAC,CAAC,MAAM,IAAI;IAC3D,IAAI,YAAY,SAAS,WAAW,YAAY,SAAS,SACvD,MAAM,IAAI,MACR,4FACF;IAEF,MAAM,QAAQ,IAAI,EAAE,SAAS,GAAG,IAAI,2BAA2B,UAAU,CAAC;IAC1E,MAAM,QAAQ,MAAM,QAAQ,IAAI,EAAE,SAAS,GAAG,IAAI,6BAA6B,UAAU,CAAC;IAE1F,MAAM,mBAAkB,MADA,QAAQ,IAAI,EAAE,SAAS,GAAG,IAAI,mCAAmC,CAAC,EAAA,CACxD,OAAO,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,OAAO,OAAO;IAC1E,IAAI,gBAAgB,WAAW,GAC7B,MAAM,IAAI,MACR,OAAO,MAAM,aAAa,YAAY,MAAM,aAAa,IACrD,yCAAyC,MAAM,UAAU,GAAA,CAAI,KAAK,MAClE,4CACN;IAEF,OAAO;KAAE,GAAG;KAAU;IAAgB;GACxC;EACF,CAAC;EACD,0BAA0B,WAAW;GACnC,aACE;GACF,aAAa,EAAE,OAAO;IACpB,QAAQ;IACR,SAAS,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAI;GACrC,CAAC;GACD,MAAM,QAAQ,EAAE,QAAQ,WAAW,KAAkD;IACnF,MAAM,QAAQ,QAAQ,GAAG;IACzB,MAAM,UAAU,MAAM,IAAI,WAAW;IACrC,MAAM,MAAM,UAAU;IAEtB,KAAI,MADgB,QAAQ,IAAI,EAAE,SAAS,GAAG,IAAI,qBAAqB,CAAC,EAAA,CAC9D,OAAO,KAAK,MAAM,IAC1B,MAAM,IAAI,MAAM,kDAAkD;IAIpE,KAAI,MAHqB,QAAQ,IAAI,EACnC,SAAS,GAAG,IAAI,mCAClB,CAAC,EAAA,CACc,OAAO,KAAK,MAAM,IAAI,MAAM,IAAI,MAAM,mCAAmC;IACxF,MAAM,EAAE,MAAM,aAAa,MAAM,QAAQ,QAAQ,GAAG;IACpD,MAAM,UAAU,MAAM,eAAe;KACnC;KACA,WAAW,QAAQ;KACnB,YAAY,SAAS;KACrB,QAAQ,SAAS;KACjB,cAAc,SAAS;KACvB,WAAW,YAAY;MACrB,MAAM,UAAU,MAAM,QAAQ,QAAQ,GAAG;MACzC,IACE,QAAQ,KAAK,YAAY,KAAK,WAC9B,QAAQ,KAAK,WAAW,cAAc,KAAK,WAAW,aACtD,QAAQ,SAAS,aAAa,SAAS,YACvC,QAAQ,SAAS,YAAY,SAAS,WACtC,QAAQ,SAAS,eAAe,SAAS,YAEzC,MAAM,IAAI,MAAM,iEAAiE;KAErF;KACA,gBAAgB;IAClB,CAAC;IACD,MAAM,YAAY,MAAM,OAAO,MAAM,WAAW,QAAQ,aAAa;KACnE,YAAY;KACZ,eAAe,KAAK;KACpB,iBAAiB,KAAK;KACtB,QAAQ;IACV,CAAC;IACD,MAAM,SAAS;KACb,SAAS;KACT,UAAU,SAAS;KACnB,OAAO,SAAS;KAChB;IACF;IACA,MAAM,OAAO,MAAM,WAAW,QAAQ,aAAa;KACjD,YAAY;KACZ,eAAe,UAAU;KACzB,iBAAiB,UAAU;KAC3B,QAAQ;KACR,QAAQ;MAAE,GAAG;MAAQ;KAAQ;IAC/B,CAAC;IACD,OAAO;GACT;EACF,CAAC;CACH;AACF"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
//#region src/model-settings.d.ts
|
|
3
|
+
/** Validates the available models and application default for one agent. */
|
|
4
|
+
declare const agentModelConfigSchema: z.ZodObject<{
|
|
5
|
+
agent: z.ZodString;
|
|
6
|
+
defaultModel: z.ZodString;
|
|
7
|
+
models: z.ZodReadonly<z.ZodArray<z.ZodObject<{
|
|
8
|
+
id: z.ZodString;
|
|
9
|
+
label: z.ZodString;
|
|
10
|
+
}, z.core.$strict>>>;
|
|
11
|
+
}, z.core.$strict>;
|
|
12
|
+
/** Validates the discovered model catalog and one repository's saved defaults. */
|
|
13
|
+
declare const modelSettingsSchema: z.ZodObject<{
|
|
14
|
+
agents: z.ZodReadonly<z.ZodArray<z.ZodObject<{
|
|
15
|
+
agent: z.ZodString;
|
|
16
|
+
defaultModel: z.ZodString;
|
|
17
|
+
models: z.ZodReadonly<z.ZodArray<z.ZodObject<{
|
|
18
|
+
id: z.ZodString;
|
|
19
|
+
label: z.ZodString;
|
|
20
|
+
}, z.core.$strict>>>;
|
|
21
|
+
}, z.core.$strict>>>;
|
|
22
|
+
repository: z.ZodNullable<z.ZodType<`${string}/${string}`, string, z.core.$ZodTypeInternals<`${string}/${string}`, string>>>;
|
|
23
|
+
defaults: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
24
|
+
revision: z.ZodNumber;
|
|
25
|
+
canEdit: z.ZodBoolean;
|
|
26
|
+
}, z.core.$strict>;
|
|
27
|
+
/** Validates an optimistic update of repository model defaults. */
|
|
28
|
+
declare const updateModelDefaultsSchema: z.ZodObject<{
|
|
29
|
+
repository: z.ZodType<`${string}/${string}`, string, z.core.$ZodTypeInternals<`${string}/${string}`, string>>;
|
|
30
|
+
defaults: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
31
|
+
expectedRevision: z.ZodNumber;
|
|
32
|
+
}, z.core.$strict>;
|
|
33
|
+
/** Application-owned model choices for an agent. */
|
|
34
|
+
type AgentModelConfig = z.infer<typeof agentModelConfigSchema>;
|
|
35
|
+
/** Model discovery and repository preference response. */
|
|
36
|
+
type ModelSettings = z.infer<typeof modelSettingsSchema>;
|
|
37
|
+
/** Compare-and-swap request for repository model preferences. */
|
|
38
|
+
type UpdateModelDefaults = z.infer<typeof updateModelDefaultsSchema>;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { AgentModelConfig, ModelSettings, UpdateModelDefaults, agentModelConfigSchema, modelSettingsSchema, updateModelDefaultsSchema };
|
|
41
|
+
//# sourceMappingURL=model-settings.d.mts.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { repositorySlugSchema } from "./schema/repository.mjs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
//#region src/model-settings.ts
|
|
4
|
+
/** Validates an opaque application-supported model identifier. */
|
|
5
|
+
const modelIdSchema = z.string().trim().min(1).max(200).describe("Application-owned model identifier.");
|
|
6
|
+
const agentId = z.string().regex(/^[a-z][a-z0-9-]{0,31}$/u);
|
|
7
|
+
/** Validates the available models and application default for one agent. */
|
|
8
|
+
const agentModelConfigSchema = z.strictObject({
|
|
9
|
+
agent: agentId.describe("Agent whose executions use these models."),
|
|
10
|
+
defaultModel: modelIdSchema.describe("Application default when the repository has no override."),
|
|
11
|
+
models: z.array(z.strictObject({
|
|
12
|
+
id: modelIdSchema,
|
|
13
|
+
label: z.string().trim().min(1).describe("Human-readable model name.")
|
|
14
|
+
})).min(1).readonly().describe("Models supported by this agent's execution adapter.")
|
|
15
|
+
}).refine((value) => new Set(value.models.map((model) => model.id)).size === value.models.length && value.models.some((model) => model.id === value.defaultModel), "Models must be unique and include the default");
|
|
16
|
+
/** Validates per-agent repository model overrides; omitted agents inherit their application default. */
|
|
17
|
+
const modelDefaultsSchema = z.record(agentId, modelIdSchema);
|
|
18
|
+
/** Validates the discovered model catalog and one repository's saved defaults. */
|
|
19
|
+
const modelSettingsSchema = z.strictObject({
|
|
20
|
+
agents: z.array(agentModelConfigSchema).readonly().describe("Application-supplied agent catalogs; empty disables model selection."),
|
|
21
|
+
repository: repositorySlugSchema.nullable().describe("Selected repository, or null for application defaults."),
|
|
22
|
+
defaults: modelDefaultsSchema.describe("Saved repository overrides."),
|
|
23
|
+
revision: z.number().int().nonnegative().describe("Repository model settings revision used to reject concurrent edits."),
|
|
24
|
+
canEdit: z.boolean().describe("Whether the authenticated caller can save defaults for this repository.")
|
|
25
|
+
});
|
|
26
|
+
/** Validates an optimistic update of repository model defaults. */
|
|
27
|
+
const updateModelDefaultsSchema = z.strictObject({
|
|
28
|
+
repository: repositorySlugSchema.describe("Configured repository to update."),
|
|
29
|
+
defaults: modelDefaultsSchema.describe("Replacement per-agent overrides; empty restores application defaults."),
|
|
30
|
+
expectedRevision: z.number().int().nonnegative().describe("Revision observed before editing.")
|
|
31
|
+
});
|
|
32
|
+
//#endregion
|
|
33
|
+
export { agentModelConfigSchema, modelSettingsSchema, updateModelDefaultsSchema };
|
|
34
|
+
|
|
35
|
+
//# sourceMappingURL=model-settings.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-settings.mjs","names":[],"sources":["../src/model-settings.ts"],"sourcesContent":["import { z } from \"zod\";\nimport { repositorySlugSchema } from \"./schema/repository\";\n\n/** Validates an opaque application-supported model identifier. */\nconst modelIdSchema = z\n .string()\n .trim()\n .min(1)\n .max(200)\n .describe(\"Application-owned model identifier.\");\nconst agentId = z.string().regex(/^[a-z][a-z0-9-]{0,31}$/u);\n\n/** Validates the available models and application default for one agent. */\nexport const agentModelConfigSchema = z\n .strictObject({\n agent: agentId.describe(\"Agent whose executions use these models.\"),\n defaultModel: modelIdSchema.describe(\n \"Application default when the repository has no override.\",\n ),\n models: z\n .array(\n z.strictObject({\n id: modelIdSchema,\n label: z.string().trim().min(1).describe(\"Human-readable model name.\"),\n }),\n )\n .min(1)\n .readonly()\n .describe(\"Models supported by this agent's execution adapter.\"),\n })\n .refine(\n (value) =>\n new Set(value.models.map((model) => model.id)).size === value.models.length &&\n value.models.some((model) => model.id === value.defaultModel),\n \"Models must be unique and include the default\",\n );\n\n/** Validates per-agent repository model overrides; omitted agents inherit their application default. */\nconst modelDefaultsSchema = z.record(agentId, modelIdSchema);\n\n/** Validates the discovered model catalog and one repository's saved defaults. */\nexport const modelSettingsSchema = z.strictObject({\n agents: z\n .array(agentModelConfigSchema)\n .readonly()\n .describe(\"Application-supplied agent catalogs; empty disables model selection.\"),\n repository: repositorySlugSchema\n .nullable()\n .describe(\"Selected repository, or null for application defaults.\"),\n defaults: modelDefaultsSchema.describe(\"Saved repository overrides.\"),\n revision: z\n .number()\n .int()\n .nonnegative()\n .describe(\"Repository model settings revision used to reject concurrent edits.\"),\n canEdit: z\n .boolean()\n .describe(\"Whether the authenticated caller can save defaults for this repository.\"),\n});\n\n/** Validates an optimistic update of repository model defaults. */\nexport const updateModelDefaultsSchema = z.strictObject({\n repository: repositorySlugSchema.describe(\"Configured repository to update.\"),\n defaults: modelDefaultsSchema.describe(\n \"Replacement per-agent overrides; empty restores application defaults.\",\n ),\n expectedRevision: z.number().int().nonnegative().describe(\"Revision observed before editing.\"),\n});\n\n/** Application-owned model choices for an agent. */\nexport type AgentModelConfig = z.infer<typeof agentModelConfigSchema>;\n/** Model discovery and repository preference response. */\nexport type ModelSettings = z.infer<typeof modelSettingsSchema>;\n/** Compare-and-swap request for repository model preferences. */\nexport type UpdateModelDefaults = z.infer<typeof updateModelDefaultsSchema>;\n"],"mappings":";;;;AAIA,MAAM,gBAAgB,EACnB,OAAO,CAAC,CACR,KAAK,CAAC,CACN,IAAI,CAAC,CAAC,CACN,IAAI,GAAG,CAAC,CACR,SAAS,qCAAqC;AACjD,MAAM,UAAU,EAAE,OAAO,CAAC,CAAC,MAAM,yBAAyB;;AAG1D,MAAa,yBAAyB,EACnC,aAAa;CACZ,OAAO,QAAQ,SAAS,0CAA0C;CAClE,cAAc,cAAc,SAC1B,0DACF;CACA,QAAQ,EACL,MACC,EAAE,aAAa;EACb,IAAI;EACJ,OAAO,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,4BAA4B;CACvE,CAAC,CACH,CAAC,CACA,IAAI,CAAC,CAAC,CACN,SAAS,CAAC,CACV,SAAS,qDAAqD;AACnE,CAAC,CAAC,CACD,QACE,UACC,IAAI,IAAI,MAAM,OAAO,KAAK,UAAU,MAAM,EAAE,CAAC,CAAC,CAAC,SAAS,MAAM,OAAO,UACrE,MAAM,OAAO,MAAM,UAAU,MAAM,OAAO,MAAM,YAAY,GAC9D,+CACF;;AAGF,MAAM,sBAAsB,EAAE,OAAO,SAAS,aAAa;;AAG3D,MAAa,sBAAsB,EAAE,aAAa;CAChD,QAAQ,EACL,MAAM,sBAAsB,CAAC,CAC7B,SAAS,CAAC,CACV,SAAS,sEAAsE;CAClF,YAAY,qBACT,SAAS,CAAC,CACV,SAAS,wDAAwD;CACpE,UAAU,oBAAoB,SAAS,6BAA6B;CACpE,UAAU,EACP,OAAO,CAAC,CACR,IAAI,CAAC,CACL,YAAY,CAAC,CACb,SAAS,qEAAqE;CACjF,SAAS,EACN,QAAQ,CAAC,CACT,SAAS,yEAAyE;AACvF,CAAC;;AAGD,MAAa,4BAA4B,EAAE,aAAa;CACtD,YAAY,qBAAqB,SAAS,kCAAkC;CAC5E,UAAU,oBAAoB,SAC5B,uEACF;CACA,kBAAkB,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC,SAAS,mCAAmC;AAC/F,CAAC"}
|
|
@@ -228,9 +228,15 @@ function plannerReconciler(options, config, state, turns) {
|
|
|
228
228
|
taskId: current.id
|
|
229
229
|
})) continue;
|
|
230
230
|
launchAttempted = true;
|
|
231
|
+
const previousDecision = activation?.finished && activation.attempt === current.attempt - 1 ? await turns.recorded(current, activation.triggerId) : null;
|
|
232
|
+
const continuation = (previousDecision?.action === "delegate" || previousDecision?.action === "wait") && !(await stores.receipts.list()).some((receipt) => receipt.taskId === current.id && receipt.attempt === current.attempt - 1 && (receipt.state === "failed" || receipt.state === "stalled")) && (await stores.messages.pending({
|
|
233
|
+
principal: runtimePrincipal,
|
|
234
|
+
toTaskId: current.id
|
|
235
|
+
})).some((message) => message.type.id === "handoff" && message.type.version === 1);
|
|
231
236
|
const launched = await dispatchTask(stores, {
|
|
232
237
|
taskId: current.id,
|
|
233
238
|
route: config.plannerRoute,
|
|
239
|
+
...continuation ? { continuationAttempt: current.attempt } : {},
|
|
234
240
|
maxLaunchAttempts: config.limits.maxTurns,
|
|
235
241
|
...options.budget ? { budget: options.budget } : {},
|
|
236
242
|
async launch(running) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconcile.mjs","names":[],"sources":["../../src/planning/reconcile.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { routeKey } from \"../agent-routes\";\nimport { BudgetExhaustedError, dispatchTask, DispatchRefusedError } from \"../dispatch\";\nimport type { TaskId } from \"../schema/id\";\nimport type { Task } from \"../schema/task\";\nimport { taskMessageIdSchema } from \"../schema/task-message\";\nimport { TaskMessageSourceFenceError } from \"../store/task-messages\";\nimport { handoffPayloadSchema, handoffTaskMessageProtocol } from \"../task-protocols\";\nimport type { PlannerConfiguration } from \"./configuration\";\nimport type { PlannerReconcileResult, PlannerRuntimeOptions } from \"./contracts\";\nimport { cancelDescendants } from \"./effects\";\nimport { plannerCancellationBatch } from \"./cancellation\";\nimport { cancelBlockedPlannerChildren } from \"./dependencies\";\nimport { runtimePrincipal, terminal, type PlannerState } from \"./state\";\nimport type { PlannerTurns } from \"./turns\";\n\nconst MAX_MESSAGES = 32;\nconst TURN_TIMEOUT_MS = 15 * 60_000;\nexport function plannerReconciler(\n options: PlannerRuntimeOptions,\n config: PlannerConfiguration,\n state: PlannerState,\n turns: PlannerTurns,\n) {\n const { stores } = options;\n return async (snapshot?: readonly Task[]): Promise<PlannerReconcileResult> => {\n const result: {\n queued: TaskId[];\n dispatched: TaskId[];\n delivered: number;\n recovered: number;\n discarded: number;\n cancelled: TaskId[];\n cancellationPending?: TaskId[];\n failed: TaskId[];\n errors: { taskId: TaskId; message: string }[];\n } = {\n queued: [],\n dispatched: [],\n delivered: 0,\n recovered: 0,\n discarded: 0,\n cancelled: [],\n failed: [],\n errors: [],\n };\n const rows = new Map((snapshot ?? (await stores.tasks.list())).map((task) => [task.id, task]));\n const errors = (taskId: TaskId, error: unknown) =>\n result.errors.push({\n taskId,\n message: (error instanceof Error ? error.message : String(error)).slice(0, 1000),\n });\n const roots = new Set(\n [...rows.values()].filter(config.isPlanner).map((task) => task.rootTaskId),\n );\n for (const root of roots) {\n try {\n for (const task of await stores.work.recover(root)) rows.set(task.id, task);\n } catch (error) {\n errors(root, error);\n }\n }\n const cancellations = plannerCancellationBatch(options);\n for (const root of roots) {\n try {\n for (const task of await cancelBlockedPlannerChildren(options, config, root)) {\n rows.set(task.id, task);\n result.cancelled.push(task.id);\n }\n } catch (error) {\n errors(root, error);\n }\n }\n const planners = [...rows.values()].filter(config.isPlanner);\n for (const planner of planners) {\n try {\n const current = await state.task(planner.id);\n if (current.state !== \"cancelled\" && current.state !== \"failed\") continue;\n result.cancelled.push(...(await cancelDescendants(options, current, cancellations)));\n } catch (error) {\n errors(planner.id, error);\n }\n }\n for (const candidate of rows.values()) {\n const parent = candidate.parentTaskId ? rows.get(candidate.parentTaskId) : undefined;\n if (!parent || !config.isPlanner(parent)) continue;\n try {\n const current = await stores.tasks.get(candidate.id);\n if (current) await cancellations.stop(current);\n } catch (error) {\n errors(candidate.id, error);\n }\n }\n let deliveryAttempts = 0;\n for (const candidate of rows.values()) {\n if (deliveryAttempts >= MAX_MESSAGES) break;\n if (\n !candidate.parentTaskId ||\n !candidate.work.route ||\n !config.allowed.has(routeKey(candidate.work.route))\n )\n continue;\n try {\n const child = await stores.tasks.get(candidate.id);\n if (!child || !terminal(child) || child.question) continue;\n const parent = await stores.tasks.get(candidate.parentTaskId);\n if (\n !parent ||\n !config.isPlanner(parent) ||\n ![\"queued\", \"running\", \"needs_human\"].includes(parent.state)\n )\n continue;\n await state.authority(parent);\n const operationId = `result:${child.id}:${child.attempt}:${child.state}`;\n const id = taskMessageIdSchema.parse(\n `msg_${createHash(\"sha256\")\n .update(JSON.stringify([child.id, operationId]))\n .digest(\"hex\")}`,\n );\n if (\n await stores.messages.get({\n principal: runtimePrincipal,\n toTaskId: parent.id,\n messageId: id,\n })\n )\n continue;\n const output = child.workResult?.output;\n // The envelope shares the 64 KiB JSON bound with its output; large results stay on the canonical Task.\n const includeOutput =\n output !== undefined && Buffer.byteLength(JSON.stringify(output), \"utf8\") <= 48_000;\n const payload = handoffPayloadSchema.parse({\n outcome: child.state === \"succeeded\" ? \"completed\" : \"failed\",\n summary: `${child.work.title}: ${child.state}`.slice(0, 1000),\n ...(includeOutput ? { output } : {}),\n artifacts: [],\n concerns:\n includeOutput || output === undefined\n ? []\n : [\"Output omitted from handoff; read canonical Task workResult.\"],\n deviations: [],\n });\n deliveryAttempts++;\n await stores.messages.sendResult({\n principal: runtimePrincipal,\n message: {\n operationId,\n fromTaskId: child.id,\n toTaskId: parent.id,\n expectAttempt: child.attempt,\n type: handoffTaskMessageProtocol.binding,\n payload,\n },\n });\n result.delivered++;\n } catch (error) {\n errors(candidate.id, error);\n }\n }\n let processed = 0;\n let launchAttempted = false;\n for (const planner of planners) {\n try {\n let current = await state.task(planner.id);\n let recoveryLeased = false;\n while (processed < MAX_MESSAGES) {\n const head = (\n await stores.messages.pending({\n principal: runtimePrincipal,\n toTaskId: current.id,\n })\n )[0];\n if (!head) break;\n if (head.type.id !== \"planner-start\") {\n const source = await stores.tasks.get(head.fromTaskId);\n if (\n !source ||\n source.attempt !== head.expectAttempt ||\n (source.state === \"cancelled\" && head.delivery !== \"result\")\n ) {\n await stores.messages.discard({\n principal: runtimePrincipal,\n toTaskId: current.id,\n messageId: head.id,\n reason: \"Source attempt changed or was cancelled; await canonical result delivery\",\n });\n result.discarded++;\n processed++;\n continue;\n }\n }\n const decision =\n current.state === \"cancelled\" ? null : await turns.recorded(current, head.id);\n if (terminal(current) && (!decision || current.state === \"cancelled\")) {\n await stores.messages.discard({\n principal: runtimePrincipal,\n toTaskId: current.id,\n messageId: head.id,\n reason: `Planner ${current.state}; no replayable committed decision`,\n });\n result.discarded++;\n processed++;\n continue;\n }\n if (!decision || ![\"running\", \"succeeded\", \"failed\"].includes(current.state)) break;\n try {\n if (current.state === \"running\")\n await turns.prepare({ taskId: current.id, attempt: current.attempt });\n const recovered = await turns.process(current, head.id, undefined, cancellations);\n if (recovered.status === \"idle\") {\n recoveryLeased = true;\n break;\n }\n result.recovered++;\n processed++;\n current = await state.task(current.id);\n } catch (error) {\n if (!(error instanceof TaskMessageSourceFenceError)) throw error;\n await stores.messages.discard({\n principal: runtimePrincipal,\n toTaskId: current.id,\n messageId: head.id,\n reason: \"Source attempt changed; discard stale result and await canonical delivery\",\n });\n result.discarded++;\n processed++;\n }\n }\n current = await state.task(current.id);\n if (terminal(current) || current.state === \"needs_human\" || recoveryLeased) continue;\n if (\n current.attempt > config.limits.maxTurns &&\n [\"queued\", \"running\", \"stalled\"].includes(current.state)\n ) {\n await stores.tasks.transition(current.id, \"failed\", {\n expectFrom: current.state,\n expectAttempt: current.attempt,\n reason: \"Planner maxTurns exhausted\",\n });\n result.failed.push(current.id);\n continue;\n }\n await state.authority(current);\n const activation = (await state.activation(current.id))?.value;\n if (current.state === \"running\") {\n const active = activation?.attempt === current.attempt ? activation : undefined;\n const since = Math.max(\n Date.parse(active?.startedAt ?? current.updatedAt),\n Date.parse(current.lastProgressAt ?? current.updatedAt),\n );\n const expired = Date.parse(state.now()) - since >= TURN_TIMEOUT_MS;\n const pending = await stores.messages.pending({\n principal: runtimePrincipal,\n toTaskId: current.id,\n });\n const handoff = pending.some(\n (message) => message.type.id === \"handoff\" && message.type.version === 1,\n );\n const unsubmitted = active && !(await turns.recorded(current, active.triggerId));\n if ((active?.finished && (handoff || unsubmitted)) || (!active?.finished && expired)) {\n if (!active?.finished && !current.waitingForMessages) {\n current = await stores.tasks.transition(current.id, \"stalled\", {\n expectFrom: \"running\",\n expectAttempt: current.attempt,\n reason: \"Planner turn or finished hook timed out\",\n phase: \"workflow\",\n });\n } else {\n current = await replace(current);\n }\n }\n }\n if (current.state === \"stalled\") current = await replace(current);\n if (current.state !== \"queued\") continue;\n if (current.attempt > config.limits.maxTurns) {\n await stores.tasks.transition(current.id, \"failed\", {\n expectFrom: \"queued\",\n expectAttempt: current.attempt,\n reason: \"Planner maxTurns exhausted\",\n });\n result.failed.push(current.id);\n continue;\n }\n if (\n launchAttempted ||\n current.approval === \"required\" ||\n current.approval === \"denied\" ||\n !(await stores.graphs.dependenciesSatisfied({\n rootTaskId: current.rootTaskId,\n taskId: current.id,\n }))\n )\n continue;\n launchAttempted = true;\n const launched = await dispatchTask(stores, {\n taskId: current.id,\n route: config.plannerRoute,\n maxLaunchAttempts: config.limits.maxTurns,\n ...(options.budget ? { budget: options.budget } : {}),\n async launch(running) {\n await state.authority(running);\n const prepared = await turns.prepare({ taskId: running.id, attempt: running.attempt });\n await state.authority(running);\n return options.launchPlanner(running, prepared.prompt);\n },\n });\n rows.set(launched.id, launched);\n result.dispatched.push(launched.id);\n } catch (error) {\n if (error instanceof BudgetExhaustedError) result.failed.push(planner.id);\n if (!(error instanceof DispatchRefusedError)) errors(planner.id, error);\n }\n }\n result.errors.push(...cancellations.errors);\n result.cancellationPending = [...cancellations.pending];\n return result;\n\n async function replace(task: Task): Promise<Task> {\n const exhausted = task.attempt >= config.limits.maxTurns;\n const next = await stores.tasks.transition(task.id, exhausted ? \"failed\" : \"queued\", {\n expectFrom: task.state,\n expectAttempt: task.attempt,\n phase: \"workflow\",\n reason: exhausted\n ? \"Planner maxTurns exhausted\"\n : \"Fresh planner activation after finished turn or timeout\",\n });\n if (exhausted) result.failed.push(task.id);\n else result.queued.push(task.id);\n rows.set(next.id, next);\n return next;\n }\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"reconcile.mjs","names":[],"sources":["../../src/planning/reconcile.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { routeKey } from \"../agent-routes\";\nimport { BudgetExhaustedError, dispatchTask, DispatchRefusedError } from \"../dispatch\";\nimport type { TaskId } from \"../schema/id\";\nimport type { Task } from \"../schema/task\";\nimport { taskMessageIdSchema } from \"../schema/task-message\";\nimport { TaskMessageSourceFenceError } from \"../store/task-messages\";\nimport { handoffPayloadSchema, handoffTaskMessageProtocol } from \"../task-protocols\";\nimport type { PlannerConfiguration } from \"./configuration\";\nimport type { PlannerReconcileResult, PlannerRuntimeOptions } from \"./contracts\";\nimport { cancelDescendants } from \"./effects\";\nimport { plannerCancellationBatch } from \"./cancellation\";\nimport { cancelBlockedPlannerChildren } from \"./dependencies\";\nimport { runtimePrincipal, terminal, type PlannerState } from \"./state\";\nimport type { PlannerTurns } from \"./turns\";\n\nconst MAX_MESSAGES = 32;\nconst TURN_TIMEOUT_MS = 15 * 60_000;\nexport function plannerReconciler(\n options: PlannerRuntimeOptions,\n config: PlannerConfiguration,\n state: PlannerState,\n turns: PlannerTurns,\n) {\n const { stores } = options;\n return async (snapshot?: readonly Task[]): Promise<PlannerReconcileResult> => {\n const result: {\n queued: TaskId[];\n dispatched: TaskId[];\n delivered: number;\n recovered: number;\n discarded: number;\n cancelled: TaskId[];\n cancellationPending?: TaskId[];\n failed: TaskId[];\n errors: { taskId: TaskId; message: string }[];\n } = {\n queued: [],\n dispatched: [],\n delivered: 0,\n recovered: 0,\n discarded: 0,\n cancelled: [],\n failed: [],\n errors: [],\n };\n const rows = new Map((snapshot ?? (await stores.tasks.list())).map((task) => [task.id, task]));\n const errors = (taskId: TaskId, error: unknown) =>\n result.errors.push({\n taskId,\n message: (error instanceof Error ? error.message : String(error)).slice(0, 1000),\n });\n const roots = new Set(\n [...rows.values()].filter(config.isPlanner).map((task) => task.rootTaskId),\n );\n for (const root of roots) {\n try {\n for (const task of await stores.work.recover(root)) rows.set(task.id, task);\n } catch (error) {\n errors(root, error);\n }\n }\n const cancellations = plannerCancellationBatch(options);\n for (const root of roots) {\n try {\n for (const task of await cancelBlockedPlannerChildren(options, config, root)) {\n rows.set(task.id, task);\n result.cancelled.push(task.id);\n }\n } catch (error) {\n errors(root, error);\n }\n }\n const planners = [...rows.values()].filter(config.isPlanner);\n for (const planner of planners) {\n try {\n const current = await state.task(planner.id);\n if (current.state !== \"cancelled\" && current.state !== \"failed\") continue;\n result.cancelled.push(...(await cancelDescendants(options, current, cancellations)));\n } catch (error) {\n errors(planner.id, error);\n }\n }\n for (const candidate of rows.values()) {\n const parent = candidate.parentTaskId ? rows.get(candidate.parentTaskId) : undefined;\n if (!parent || !config.isPlanner(parent)) continue;\n try {\n const current = await stores.tasks.get(candidate.id);\n if (current) await cancellations.stop(current);\n } catch (error) {\n errors(candidate.id, error);\n }\n }\n let deliveryAttempts = 0;\n for (const candidate of rows.values()) {\n if (deliveryAttempts >= MAX_MESSAGES) break;\n if (\n !candidate.parentTaskId ||\n !candidate.work.route ||\n !config.allowed.has(routeKey(candidate.work.route))\n )\n continue;\n try {\n const child = await stores.tasks.get(candidate.id);\n if (!child || !terminal(child) || child.question) continue;\n const parent = await stores.tasks.get(candidate.parentTaskId);\n if (\n !parent ||\n !config.isPlanner(parent) ||\n ![\"queued\", \"running\", \"needs_human\"].includes(parent.state)\n )\n continue;\n await state.authority(parent);\n const operationId = `result:${child.id}:${child.attempt}:${child.state}`;\n const id = taskMessageIdSchema.parse(\n `msg_${createHash(\"sha256\")\n .update(JSON.stringify([child.id, operationId]))\n .digest(\"hex\")}`,\n );\n if (\n await stores.messages.get({\n principal: runtimePrincipal,\n toTaskId: parent.id,\n messageId: id,\n })\n )\n continue;\n const output = child.workResult?.output;\n // The envelope shares the 64 KiB JSON bound with its output; large results stay on the canonical Task.\n const includeOutput =\n output !== undefined && Buffer.byteLength(JSON.stringify(output), \"utf8\") <= 48_000;\n const payload = handoffPayloadSchema.parse({\n outcome: child.state === \"succeeded\" ? \"completed\" : \"failed\",\n summary: `${child.work.title}: ${child.state}`.slice(0, 1000),\n ...(includeOutput ? { output } : {}),\n artifacts: [],\n concerns:\n includeOutput || output === undefined\n ? []\n : [\"Output omitted from handoff; read canonical Task workResult.\"],\n deviations: [],\n });\n deliveryAttempts++;\n await stores.messages.sendResult({\n principal: runtimePrincipal,\n message: {\n operationId,\n fromTaskId: child.id,\n toTaskId: parent.id,\n expectAttempt: child.attempt,\n type: handoffTaskMessageProtocol.binding,\n payload,\n },\n });\n result.delivered++;\n } catch (error) {\n errors(candidate.id, error);\n }\n }\n let processed = 0;\n let launchAttempted = false;\n for (const planner of planners) {\n try {\n let current = await state.task(planner.id);\n let recoveryLeased = false;\n while (processed < MAX_MESSAGES) {\n const head = (\n await stores.messages.pending({\n principal: runtimePrincipal,\n toTaskId: current.id,\n })\n )[0];\n if (!head) break;\n if (head.type.id !== \"planner-start\") {\n const source = await stores.tasks.get(head.fromTaskId);\n if (\n !source ||\n source.attempt !== head.expectAttempt ||\n (source.state === \"cancelled\" && head.delivery !== \"result\")\n ) {\n await stores.messages.discard({\n principal: runtimePrincipal,\n toTaskId: current.id,\n messageId: head.id,\n reason: \"Source attempt changed or was cancelled; await canonical result delivery\",\n });\n result.discarded++;\n processed++;\n continue;\n }\n }\n const decision =\n current.state === \"cancelled\" ? null : await turns.recorded(current, head.id);\n if (terminal(current) && (!decision || current.state === \"cancelled\")) {\n await stores.messages.discard({\n principal: runtimePrincipal,\n toTaskId: current.id,\n messageId: head.id,\n reason: `Planner ${current.state}; no replayable committed decision`,\n });\n result.discarded++;\n processed++;\n continue;\n }\n if (!decision || ![\"running\", \"succeeded\", \"failed\"].includes(current.state)) break;\n try {\n if (current.state === \"running\")\n await turns.prepare({ taskId: current.id, attempt: current.attempt });\n const recovered = await turns.process(current, head.id, undefined, cancellations);\n if (recovered.status === \"idle\") {\n recoveryLeased = true;\n break;\n }\n result.recovered++;\n processed++;\n current = await state.task(current.id);\n } catch (error) {\n if (!(error instanceof TaskMessageSourceFenceError)) throw error;\n await stores.messages.discard({\n principal: runtimePrincipal,\n toTaskId: current.id,\n messageId: head.id,\n reason: \"Source attempt changed; discard stale result and await canonical delivery\",\n });\n result.discarded++;\n processed++;\n }\n }\n current = await state.task(current.id);\n if (terminal(current) || current.state === \"needs_human\" || recoveryLeased) continue;\n if (\n current.attempt > config.limits.maxTurns &&\n [\"queued\", \"running\", \"stalled\"].includes(current.state)\n ) {\n await stores.tasks.transition(current.id, \"failed\", {\n expectFrom: current.state,\n expectAttempt: current.attempt,\n reason: \"Planner maxTurns exhausted\",\n });\n result.failed.push(current.id);\n continue;\n }\n await state.authority(current);\n const activation = (await state.activation(current.id))?.value;\n if (current.state === \"running\") {\n const active = activation?.attempt === current.attempt ? activation : undefined;\n const since = Math.max(\n Date.parse(active?.startedAt ?? current.updatedAt),\n Date.parse(current.lastProgressAt ?? current.updatedAt),\n );\n const expired = Date.parse(state.now()) - since >= TURN_TIMEOUT_MS;\n const pending = await stores.messages.pending({\n principal: runtimePrincipal,\n toTaskId: current.id,\n });\n const handoff = pending.some(\n (message) => message.type.id === \"handoff\" && message.type.version === 1,\n );\n const unsubmitted = active && !(await turns.recorded(current, active.triggerId));\n if ((active?.finished && (handoff || unsubmitted)) || (!active?.finished && expired)) {\n if (!active?.finished && !current.waitingForMessages) {\n current = await stores.tasks.transition(current.id, \"stalled\", {\n expectFrom: \"running\",\n expectAttempt: current.attempt,\n reason: \"Planner turn or finished hook timed out\",\n phase: \"workflow\",\n });\n } else {\n current = await replace(current);\n }\n }\n }\n if (current.state === \"stalled\") current = await replace(current);\n if (current.state !== \"queued\") continue;\n if (current.attempt > config.limits.maxTurns) {\n await stores.tasks.transition(current.id, \"failed\", {\n expectFrom: \"queued\",\n expectAttempt: current.attempt,\n reason: \"Planner maxTurns exhausted\",\n });\n result.failed.push(current.id);\n continue;\n }\n if (\n launchAttempted ||\n current.approval === \"required\" ||\n current.approval === \"denied\" ||\n !(await stores.graphs.dependenciesSatisfied({\n rootTaskId: current.rootTaskId,\n taskId: current.id,\n }))\n )\n continue;\n launchAttempted = true;\n const previousDecision =\n activation?.finished && activation.attempt === current.attempt - 1\n ? await turns.recorded(current, activation.triggerId)\n : null;\n const continuation =\n (previousDecision?.action === \"delegate\" || previousDecision?.action === \"wait\") &&\n !(await stores.receipts.list()).some(\n (receipt) =>\n receipt.taskId === current.id &&\n receipt.attempt === current.attempt - 1 &&\n (receipt.state === \"failed\" || receipt.state === \"stalled\"),\n ) &&\n (\n await stores.messages.pending({\n principal: runtimePrincipal,\n toTaskId: current.id,\n })\n ).some((message) => message.type.id === \"handoff\" && message.type.version === 1);\n const launched = await dispatchTask(stores, {\n taskId: current.id,\n route: config.plannerRoute,\n ...(continuation ? { continuationAttempt: current.attempt } : {}),\n maxLaunchAttempts: config.limits.maxTurns,\n ...(options.budget ? { budget: options.budget } : {}),\n async launch(running) {\n await state.authority(running);\n const prepared = await turns.prepare({ taskId: running.id, attempt: running.attempt });\n await state.authority(running);\n return options.launchPlanner(running, prepared.prompt);\n },\n });\n rows.set(launched.id, launched);\n result.dispatched.push(launched.id);\n } catch (error) {\n if (error instanceof BudgetExhaustedError) result.failed.push(planner.id);\n if (!(error instanceof DispatchRefusedError)) errors(planner.id, error);\n }\n }\n result.errors.push(...cancellations.errors);\n result.cancellationPending = [...cancellations.pending];\n return result;\n\n async function replace(task: Task): Promise<Task> {\n const exhausted = task.attempt >= config.limits.maxTurns;\n const next = await stores.tasks.transition(task.id, exhausted ? \"failed\" : \"queued\", {\n expectFrom: task.state,\n expectAttempt: task.attempt,\n phase: \"workflow\",\n reason: exhausted\n ? \"Planner maxTurns exhausted\"\n : \"Fresh planner activation after finished turn or timeout\",\n });\n if (exhausted) result.failed.push(task.id);\n else result.queued.push(task.id);\n rows.set(next.id, next);\n return next;\n }\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
1
2
|
import { StoreDriver } from "../store/driver.mjs";
|
|
2
3
|
import { PgDatabase, PgQueryResultHKT } from "drizzle-orm/pg-core";
|
|
3
4
|
//#region src/postgres/index.d.ts
|
|
@@ -125,7 +126,9 @@ declare const factoryMigrationsLedger: import("drizzle-orm/pg-core").PgTableWith
|
|
|
125
126
|
}>;
|
|
126
127
|
/** One immutable, idempotent SQL migration identified by its stable tag. */
|
|
127
128
|
interface FactoryMigration {
|
|
129
|
+
/** Stable append-only ledger key; shipped tags must never be renamed or reordered. */
|
|
128
130
|
readonly tag: string;
|
|
131
|
+
/** Idempotent SQL statements executed in order before the tag is recorded. */
|
|
129
132
|
readonly statements: readonly string[];
|
|
130
133
|
}
|
|
131
134
|
/**
|
|
@@ -134,9 +137,47 @@ interface FactoryMigration {
|
|
|
134
137
|
* them on retry. Never edit or reorder an entry that has shipped.
|
|
135
138
|
*/
|
|
136
139
|
declare const factoryMigrations: readonly FactoryMigration[];
|
|
137
|
-
/**
|
|
140
|
+
/**
|
|
141
|
+
* Applies every unapplied checked-in Factory schema migration.
|
|
142
|
+
*
|
|
143
|
+
* @remarks
|
|
144
|
+
* Call this explicitly during deployment or process startup before serving Factory operations.
|
|
145
|
+
* Statements run in listed order and the migration tag is recorded afterward. Because those
|
|
146
|
+
* writes are not one interactive transaction, every statement is idempotent: if the process stops
|
|
147
|
+
* between a statement and its ledger insert, rerunning this function safely attempts that migration
|
|
148
|
+
* again. The deployment or startup owner must serialize migration calls for a database; HTTP
|
|
149
|
+
* database drivers may not support the connection-scoped locking needed to do that here.
|
|
150
|
+
*
|
|
151
|
+
* These migrations create and evolve Factory SQL tables only. They do not reinterpret or migrate
|
|
152
|
+
* older JSON entity shapes stored in `factory_records`; releases that change persisted entity
|
|
153
|
+
* schemas still require an explicit application data migration or reset decision.
|
|
154
|
+
*
|
|
155
|
+
* The caller owns the database connection and should retry the whole call after resolving a SQL
|
|
156
|
+
* failure. A rejected promise means startup must not assume all tables are ready.
|
|
157
|
+
*
|
|
158
|
+
* @param db - Drizzle PostgreSQL database with DDL access for Factory migrations.
|
|
159
|
+
* @returns Tags this invocation found unapplied and ran, in checked-in order.
|
|
160
|
+
* @throws When ledger creation, a migration statement, or ledger persistence fails.
|
|
161
|
+
*/
|
|
138
162
|
declare function applyFactoryMigrations(db: FactoryDatabase): Promise<readonly string[]>;
|
|
139
|
-
/**
|
|
163
|
+
/**
|
|
164
|
+
* Creates an atomic compare-and-swap driver over the Factory PostgreSQL tables.
|
|
165
|
+
*
|
|
166
|
+
* @remarks
|
|
167
|
+
* The caller must apply {@link applyFactoryMigrations} before serving Factory operations. Inserts
|
|
168
|
+
* use the table's primary key for create-if-absent behavior; updates require the expected version
|
|
169
|
+
* and throw `VersionConflictError` when another writer wins. The driver never creates schema at
|
|
170
|
+
* runtime and does not own the database connection lifecycle.
|
|
171
|
+
* Reads return the latest committed row visible to the supplied connection. The engine validates
|
|
172
|
+
* stored values and retries only the conflicts its operation explicitly supports; callers should
|
|
173
|
+
* surface other database failures and reconcile from canonical state before repeating effects.
|
|
174
|
+
*
|
|
175
|
+
* @param db - Drizzle PostgreSQL database connected with DML access to the migrated tables.
|
|
176
|
+
* @returns A `postgres` driver suitable for `createStores`.
|
|
177
|
+
* @throws Store operations throw `RecordExistsError`, `RecordNotFoundError`, or
|
|
178
|
+
* `VersionConflictError` for their corresponding persistence races, plus underlying database
|
|
179
|
+
* errors for unavailable or rejected queries.
|
|
180
|
+
*/
|
|
140
181
|
declare function createPostgresDriver(db: FactoryDatabase): StoreDriver;
|
|
141
182
|
//#endregion
|
|
142
183
|
export { FactoryDatabase, FactoryMigration, applyFactoryMigrations, createPostgresDriver, factoryMigrations, factoryMigrationsLedger, factoryRecords };
|
package/dist/postgres/index.mjs
CHANGED
|
@@ -33,7 +33,28 @@ const migrationsLedgerDdl = `CREATE TABLE IF NOT EXISTS "factory_migrations" (
|
|
|
33
33
|
"tag" text PRIMARY KEY,
|
|
34
34
|
"applied_at" timestamptz NOT NULL DEFAULT now()
|
|
35
35
|
)`;
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Applies every unapplied checked-in Factory schema migration.
|
|
38
|
+
*
|
|
39
|
+
* @remarks
|
|
40
|
+
* Call this explicitly during deployment or process startup before serving Factory operations.
|
|
41
|
+
* Statements run in listed order and the migration tag is recorded afterward. Because those
|
|
42
|
+
* writes are not one interactive transaction, every statement is idempotent: if the process stops
|
|
43
|
+
* between a statement and its ledger insert, rerunning this function safely attempts that migration
|
|
44
|
+
* again. The deployment or startup owner must serialize migration calls for a database; HTTP
|
|
45
|
+
* database drivers may not support the connection-scoped locking needed to do that here.
|
|
46
|
+
*
|
|
47
|
+
* These migrations create and evolve Factory SQL tables only. They do not reinterpret or migrate
|
|
48
|
+
* older JSON entity shapes stored in `factory_records`; releases that change persisted entity
|
|
49
|
+
* schemas still require an explicit application data migration or reset decision.
|
|
50
|
+
*
|
|
51
|
+
* The caller owns the database connection and should retry the whole call after resolving a SQL
|
|
52
|
+
* failure. A rejected promise means startup must not assume all tables are ready.
|
|
53
|
+
*
|
|
54
|
+
* @param db - Drizzle PostgreSQL database with DDL access for Factory migrations.
|
|
55
|
+
* @returns Tags this invocation found unapplied and ran, in checked-in order.
|
|
56
|
+
* @throws When ledger creation, a migration statement, or ledger persistence fails.
|
|
57
|
+
*/
|
|
37
58
|
async function applyFactoryMigrations(db) {
|
|
38
59
|
await db.execute(sql.raw(migrationsLedgerDdl));
|
|
39
60
|
const applied = [];
|
|
@@ -45,7 +66,24 @@ async function applyFactoryMigrations(db) {
|
|
|
45
66
|
}
|
|
46
67
|
return applied;
|
|
47
68
|
}
|
|
48
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Creates an atomic compare-and-swap driver over the Factory PostgreSQL tables.
|
|
71
|
+
*
|
|
72
|
+
* @remarks
|
|
73
|
+
* The caller must apply {@link applyFactoryMigrations} before serving Factory operations. Inserts
|
|
74
|
+
* use the table's primary key for create-if-absent behavior; updates require the expected version
|
|
75
|
+
* and throw `VersionConflictError` when another writer wins. The driver never creates schema at
|
|
76
|
+
* runtime and does not own the database connection lifecycle.
|
|
77
|
+
* Reads return the latest committed row visible to the supplied connection. The engine validates
|
|
78
|
+
* stored values and retries only the conflicts its operation explicitly supports; callers should
|
|
79
|
+
* surface other database failures and reconcile from canonical state before repeating effects.
|
|
80
|
+
*
|
|
81
|
+
* @param db - Drizzle PostgreSQL database connected with DML access to the migrated tables.
|
|
82
|
+
* @returns A `postgres` driver suitable for `createStores`.
|
|
83
|
+
* @throws Store operations throw `RecordExistsError`, `RecordNotFoundError`, or
|
|
84
|
+
* `VersionConflictError` for their corresponding persistence races, plus underlying database
|
|
85
|
+
* errors for unavailable or rejected queries.
|
|
86
|
+
*/
|
|
49
87
|
function createPostgresDriver(db) {
|
|
50
88
|
async function readRecord(collection, id) {
|
|
51
89
|
const row = (await db.select({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/postgres/index.ts"],"sourcesContent":["import { and, eq, sql } from \"drizzle-orm\";\nimport {\n integer,\n jsonb,\n pgTable,\n primaryKey,\n text,\n timestamp,\n type PgDatabase,\n type PgQueryResultHKT,\n} from \"drizzle-orm/pg-core\";\nimport {\n RecordExistsError,\n RecordNotFoundError,\n VersionConflictError,\n type CollectionName,\n type StoreDriver,\n} from \"../store/driver\";\n\n/** Drizzle PostgreSQL database shape required by the Factory driver and migrations. */\nexport type FactoryDatabase = PgDatabase<PgQueryResultHKT>;\n\n/** Shared versioned-record table used for every Factory store collection. */\nexport const factoryRecords = pgTable(\n \"factory_records\",\n {\n collection: text(\"collection\").notNull(),\n id: text(\"id\").notNull(),\n value: jsonb(\"value\").$type<unknown>().notNull(),\n version: integer(\"version\").notNull(),\n },\n (table) => [primaryKey({ columns: [table.collection, table.id] })],\n);\n\n/** Append-only ledger of checked-in Factory migrations already applied to a database. */\nexport const factoryMigrationsLedger = pgTable(\"factory_migrations\", {\n tag: text(\"tag\").primaryKey(),\n appliedAt: timestamp(\"applied_at\", { withTimezone: true }).notNull().defaultNow(),\n});\n\n/** One immutable, idempotent SQL migration identified by its stable tag. */\nexport interface FactoryMigration {\n readonly tag: string;\n readonly statements: readonly string[];\n}\n\n/**\n * Checked-in, append-only migration list. Statements must be idempotent:\n * they run before the ledger records them, so an interrupted run re-executes\n * them on retry. Never edit or reorder an entry that has shipped.\n */\nexport const factoryMigrations: readonly FactoryMigration[] = [\n {\n tag: \"0000_factory_records\",\n statements: [\n `CREATE TABLE IF NOT EXISTS \"factory_records\" (\n \"collection\" text NOT NULL,\n \"id\" text NOT NULL,\n \"value\" jsonb NOT NULL,\n \"version\" integer NOT NULL,\n CONSTRAINT \"factory_records_pk\" PRIMARY KEY (\"collection\", \"id\")\n )`,\n ],\n },\n];\n\nconst migrationsLedgerDdl = `CREATE TABLE IF NOT EXISTS \"factory_migrations\" (\n \"tag\" text PRIMARY KEY,\n \"applied_at\" timestamptz NOT NULL DEFAULT now()\n)`;\n\n/** Apply each unapplied checked-in migration and return the tags recorded by this call. */\nexport async function applyFactoryMigrations(db: FactoryDatabase): Promise<readonly string[]> {\n await db.execute(sql.raw(migrationsLedgerDdl));\n const applied: string[] = [];\n for (const migration of factoryMigrations) {\n const existing = await db\n .select({ tag: factoryMigrationsLedger.tag })\n .from(factoryMigrationsLedger)\n .where(eq(factoryMigrationsLedger.tag, migration.tag))\n .limit(1);\n if (existing.length > 0) {\n continue;\n }\n for (const statement of migration.statements) {\n await db.execute(sql.raw(statement));\n }\n await db.insert(factoryMigrationsLedger).values({ tag: migration.tag }).onConflictDoNothing();\n applied.push(migration.tag);\n }\n return applied;\n}\n\n/** Create an atomic compare-and-swap StoreDriver over the Factory PostgreSQL tables. */\nexport function createPostgresDriver(db: FactoryDatabase): StoreDriver {\n async function readRecord(\n collection: CollectionName,\n id: string,\n ): Promise<{ value: unknown; version: number } | null> {\n const rows = await db\n .select({ value: factoryRecords.value, version: factoryRecords.version })\n .from(factoryRecords)\n .where(and(eq(factoryRecords.collection, collection), eq(factoryRecords.id, id)))\n .limit(1);\n const row = rows[0];\n return row === undefined ? null : { value: row.value, version: row.version };\n }\n\n return {\n name: \"postgres\",\n\n get(collection, id) {\n return readRecord(collection, id);\n },\n\n async insert({ collection, id, value }) {\n const inserted = await db\n .insert(factoryRecords)\n .values({ collection, id, value, version: 1 })\n .onConflictDoNothing()\n .returning({ id: factoryRecords.id });\n if (inserted.length === 0) {\n throw new RecordExistsError(collection, id);\n }\n },\n\n async update({ collection, id, value, expectedVersion }) {\n const updated = await db\n .update(factoryRecords)\n .set({ value, version: expectedVersion + 1 })\n .where(\n and(\n eq(factoryRecords.collection, collection),\n eq(factoryRecords.id, id),\n eq(factoryRecords.version, expectedVersion),\n ),\n )\n .returning({ id: factoryRecords.id });\n if (updated.length === 0) {\n const existing = await readRecord(collection, id);\n if (existing === null) {\n throw new RecordNotFoundError(collection, id);\n }\n throw new VersionConflictError(collection, id, expectedVersion);\n }\n },\n\n async list(collection) {\n const rows = await db\n .select({\n id: factoryRecords.id,\n value: factoryRecords.value,\n version: factoryRecords.version,\n })\n .from(factoryRecords)\n .where(eq(factoryRecords.collection, collection));\n return rows.map((row) => ({\n id: row.id,\n record: { value: row.value, version: row.version },\n }));\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/postgres/index.ts"],"sourcesContent":["import { and, eq, sql } from \"drizzle-orm\";\nimport {\n integer,\n jsonb,\n pgTable,\n primaryKey,\n text,\n timestamp,\n type PgDatabase,\n type PgQueryResultHKT,\n} from \"drizzle-orm/pg-core\";\nimport {\n RecordExistsError,\n RecordNotFoundError,\n VersionConflictError,\n type CollectionName,\n type StoreDriver,\n} from \"../store/driver\";\n\n/** Drizzle PostgreSQL database shape required by the Factory driver and migrations. */\nexport type FactoryDatabase = PgDatabase<PgQueryResultHKT>;\n\n/** Shared versioned-record table used for every Factory store collection. */\nexport const factoryRecords = pgTable(\n \"factory_records\",\n {\n collection: text(\"collection\").notNull(),\n id: text(\"id\").notNull(),\n value: jsonb(\"value\").$type<unknown>().notNull(),\n version: integer(\"version\").notNull(),\n },\n (table) => [primaryKey({ columns: [table.collection, table.id] })],\n);\n\n/** Append-only ledger of checked-in Factory migrations already applied to a database. */\nexport const factoryMigrationsLedger = pgTable(\"factory_migrations\", {\n tag: text(\"tag\").primaryKey(),\n appliedAt: timestamp(\"applied_at\", { withTimezone: true }).notNull().defaultNow(),\n});\n\n/** One immutable, idempotent SQL migration identified by its stable tag. */\nexport interface FactoryMigration {\n /** Stable append-only ledger key; shipped tags must never be renamed or reordered. */\n readonly tag: string;\n /** Idempotent SQL statements executed in order before the tag is recorded. */\n readonly statements: readonly string[];\n}\n\n/**\n * Checked-in, append-only migration list. Statements must be idempotent:\n * they run before the ledger records them, so an interrupted run re-executes\n * them on retry. Never edit or reorder an entry that has shipped.\n */\nexport const factoryMigrations: readonly FactoryMigration[] = [\n {\n tag: \"0000_factory_records\",\n statements: [\n `CREATE TABLE IF NOT EXISTS \"factory_records\" (\n \"collection\" text NOT NULL,\n \"id\" text NOT NULL,\n \"value\" jsonb NOT NULL,\n \"version\" integer NOT NULL,\n CONSTRAINT \"factory_records_pk\" PRIMARY KEY (\"collection\", \"id\")\n )`,\n ],\n },\n];\n\nconst migrationsLedgerDdl = `CREATE TABLE IF NOT EXISTS \"factory_migrations\" (\n \"tag\" text PRIMARY KEY,\n \"applied_at\" timestamptz NOT NULL DEFAULT now()\n)`;\n\n/**\n * Applies every unapplied checked-in Factory schema migration.\n *\n * @remarks\n * Call this explicitly during deployment or process startup before serving Factory operations.\n * Statements run in listed order and the migration tag is recorded afterward. Because those\n * writes are not one interactive transaction, every statement is idempotent: if the process stops\n * between a statement and its ledger insert, rerunning this function safely attempts that migration\n * again. The deployment or startup owner must serialize migration calls for a database; HTTP\n * database drivers may not support the connection-scoped locking needed to do that here.\n *\n * These migrations create and evolve Factory SQL tables only. They do not reinterpret or migrate\n * older JSON entity shapes stored in `factory_records`; releases that change persisted entity\n * schemas still require an explicit application data migration or reset decision.\n *\n * The caller owns the database connection and should retry the whole call after resolving a SQL\n * failure. A rejected promise means startup must not assume all tables are ready.\n *\n * @param db - Drizzle PostgreSQL database with DDL access for Factory migrations.\n * @returns Tags this invocation found unapplied and ran, in checked-in order.\n * @throws When ledger creation, a migration statement, or ledger persistence fails.\n */\nexport async function applyFactoryMigrations(db: FactoryDatabase): Promise<readonly string[]> {\n await db.execute(sql.raw(migrationsLedgerDdl));\n const applied: string[] = [];\n for (const migration of factoryMigrations) {\n const existing = await db\n .select({ tag: factoryMigrationsLedger.tag })\n .from(factoryMigrationsLedger)\n .where(eq(factoryMigrationsLedger.tag, migration.tag))\n .limit(1);\n if (existing.length > 0) {\n continue;\n }\n for (const statement of migration.statements) {\n await db.execute(sql.raw(statement));\n }\n await db.insert(factoryMigrationsLedger).values({ tag: migration.tag }).onConflictDoNothing();\n applied.push(migration.tag);\n }\n return applied;\n}\n\n/**\n * Creates an atomic compare-and-swap driver over the Factory PostgreSQL tables.\n *\n * @remarks\n * The caller must apply {@link applyFactoryMigrations} before serving Factory operations. Inserts\n * use the table's primary key for create-if-absent behavior; updates require the expected version\n * and throw `VersionConflictError` when another writer wins. The driver never creates schema at\n * runtime and does not own the database connection lifecycle.\n * Reads return the latest committed row visible to the supplied connection. The engine validates\n * stored values and retries only the conflicts its operation explicitly supports; callers should\n * surface other database failures and reconcile from canonical state before repeating effects.\n *\n * @param db - Drizzle PostgreSQL database connected with DML access to the migrated tables.\n * @returns A `postgres` driver suitable for `createStores`.\n * @throws Store operations throw `RecordExistsError`, `RecordNotFoundError`, or\n * `VersionConflictError` for their corresponding persistence races, plus underlying database\n * errors for unavailable or rejected queries.\n */\nexport function createPostgresDriver(db: FactoryDatabase): StoreDriver {\n async function readRecord(\n collection: CollectionName,\n id: string,\n ): Promise<{ value: unknown; version: number } | null> {\n const rows = await db\n .select({ value: factoryRecords.value, version: factoryRecords.version })\n .from(factoryRecords)\n .where(and(eq(factoryRecords.collection, collection), eq(factoryRecords.id, id)))\n .limit(1);\n const row = rows[0];\n return row === undefined ? null : { value: row.value, version: row.version };\n }\n\n return {\n name: \"postgres\",\n\n get(collection, id) {\n return readRecord(collection, id);\n },\n\n async insert({ collection, id, value }) {\n const inserted = await db\n .insert(factoryRecords)\n .values({ collection, id, value, version: 1 })\n .onConflictDoNothing()\n .returning({ id: factoryRecords.id });\n if (inserted.length === 0) {\n throw new RecordExistsError(collection, id);\n }\n },\n\n async update({ collection, id, value, expectedVersion }) {\n const updated = await db\n .update(factoryRecords)\n .set({ value, version: expectedVersion + 1 })\n .where(\n and(\n eq(factoryRecords.collection, collection),\n eq(factoryRecords.id, id),\n eq(factoryRecords.version, expectedVersion),\n ),\n )\n .returning({ id: factoryRecords.id });\n if (updated.length === 0) {\n const existing = await readRecord(collection, id);\n if (existing === null) {\n throw new RecordNotFoundError(collection, id);\n }\n throw new VersionConflictError(collection, id, expectedVersion);\n }\n },\n\n async list(collection) {\n const rows = await db\n .select({\n id: factoryRecords.id,\n value: factoryRecords.value,\n version: factoryRecords.version,\n })\n .from(factoryRecords)\n .where(eq(factoryRecords.collection, collection));\n return rows.map((row) => ({\n id: row.id,\n record: { value: row.value, version: row.version },\n }));\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -6,6 +6,7 @@ import { WorkNotification } from "../../store/work-notifications.mjs";
|
|
|
6
6
|
import { MillisecondClock } from "../../schema/common.mjs";
|
|
7
7
|
import { FactoryStores } from "../../store/engine.mjs";
|
|
8
8
|
import { DispatchBudget } from "../../dispatch.mjs";
|
|
9
|
+
import { PresentVerificationResultInput } from "../../change-verification/eve-tool.mjs";
|
|
9
10
|
import { TaskCommunicationReconciler } from "../../task-communication.mjs";
|
|
10
11
|
import { SweepQueuedOptions, SweepQueuedResult } from "../../sweep.mjs";
|
|
11
12
|
import { PlannerReconcileResult } from "../../planning/contracts.mjs";
|
|
@@ -13,7 +14,7 @@ import "../../planning/runtime.mjs";
|
|
|
13
14
|
import { SoftwareDevelopmentTaskCommunicationStores } from "./task-communication.mjs";
|
|
14
15
|
//#region src/presets/software-development/dispatch.d.ts
|
|
15
16
|
/** Factory capabilities composed by the supplied software-development dispatcher. */
|
|
16
|
-
type SoftwareDevelopmentDispatchStores = Pick<FactoryStores, "changes" | "graphs" | "outbox" | "receipts" | "repositories" | "sessions" | "tasks" | "work">;
|
|
17
|
+
type SoftwareDevelopmentDispatchStores = Pick<FactoryStores, "changes" | "graphs" | "outbox" | "receipts" | "repositories" | "sessions" | "signals" | "tasks" | "work">;
|
|
17
18
|
/** Application policies and adapters for reconciling one reference-work root graph. */
|
|
18
19
|
interface DispatchSoftwareDevelopmentWorkOptions {
|
|
19
20
|
/** Durable wakeup identifying the canonical Task graph to reconcile. */
|
|
@@ -28,6 +29,8 @@ interface DispatchSoftwareDevelopmentWorkOptions {
|
|
|
28
29
|
launchers: SweepQueuedOptions["launchers"];
|
|
29
30
|
budget: DispatchBudget;
|
|
30
31
|
verificationRoute?: AgentRouteBinding;
|
|
32
|
+
/** Idempotent application presenter for recorded verification outcomes recovered by the queue. */
|
|
33
|
+
presentVerificationResult?: (input: PresentVerificationResultInput) => Promise<void>;
|
|
31
34
|
canUseRepository(id: RepositoryId): Promise<boolean>;
|
|
32
35
|
publish(event: WorkNotification, delaySeconds?: number): Promise<void>;
|
|
33
36
|
/**
|
|
@@ -24,7 +24,8 @@ async function dispatchSoftwareDevelopmentWork(options) {
|
|
|
24
24
|
if (options.verificationRoute) await ensureChangeVerificationTasks({
|
|
25
25
|
stores,
|
|
26
26
|
tasks: await recover(),
|
|
27
|
-
route: options.verificationRoute
|
|
27
|
+
route: options.verificationRoute,
|
|
28
|
+
presentResult: options.presentVerificationResult
|
|
28
29
|
});
|
|
29
30
|
await sweepSettlements(stores, { tasks: await recover() });
|
|
30
31
|
const planner = await planning.reconcile(await recover());
|