@vercel/factory 0.0.14 → 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 +96 -2
- package/dist/agent-routes.d.mts +113 -0
- package/dist/agent-routes.mjs +91 -0
- package/dist/agent-routes.mjs.map +1 -0
- package/dist/api-contracts.d.mts +1768 -0
- package/dist/api-contracts.mjs +119 -0
- package/dist/api-contracts.mjs.map +1 -0
- package/dist/api-images.d.mts +30 -0
- package/dist/api-images.mjs +28 -0
- package/dist/api-images.mjs.map +1 -0
- package/dist/api-task-graph.mjs +57 -0
- package/dist/api-task-graph.mjs.map +1 -0
- package/dist/api.d.mts +111 -216
- package/dist/api.mjs +1063 -99
- package/dist/api.mjs.map +1 -1
- package/dist/approval-access.d.mts +39 -0
- package/dist/approval-access.mjs +62 -0
- package/dist/approval-access.mjs.map +1 -0
- package/dist/approval-contracts.d.mts +698 -0
- package/dist/approval-contracts.mjs +27 -0
- package/dist/approval-contracts.mjs.map +1 -0
- package/dist/approval.d.mts +49 -0
- package/dist/approval.mjs +39 -0
- package/dist/approval.mjs.map +1 -0
- package/dist/blob/artifacts.d.mts +37 -0
- package/dist/blob/artifacts.mjs +45 -0
- package/dist/blob/artifacts.mjs.map +1 -0
- package/dist/blob/index.d.mts +84 -12
- package/dist/blob/index.mjs +179 -45
- package/dist/blob/index.mjs.map +1 -1
- package/dist/budget.d.mts +115 -0
- package/dist/budget.mjs +147 -0
- package/dist/budget.mjs.map +1 -0
- package/dist/budgets.d.mts +4 -0
- package/dist/budgets.mjs +4 -0
- package/dist/build-factory.d.mts +7 -4
- package/dist/build-factory.mjs +2 -1
- package/dist/build-factory.mjs.map +1 -1
- package/dist/change-verification/brief.d.mts +9 -0
- package/dist/change-verification/brief.mjs +13 -0
- package/dist/change-verification/brief.mjs.map +1 -0
- package/dist/change-verification/dispatch.d.mts +37 -0
- package/dist/change-verification/dispatch.mjs +83 -0
- package/dist/change-verification/dispatch.mjs.map +1 -0
- package/dist/change-verification/eve-tool.d.mts +61 -0
- package/dist/change-verification/eve-tool.mjs +89 -0
- package/dist/change-verification/eve-tool.mjs.map +1 -0
- 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 +56 -0
- package/dist/changes/eve-record-change.mjs +131 -0
- package/dist/changes/eve-record-change.mjs.map +1 -0
- package/dist/changes/presentation.d.mts +14 -0
- package/dist/changes.d.mts +24 -0
- package/dist/changes.mjs +29 -0
- package/dist/changes.mjs.map +1 -0
- package/dist/client-events.d.mts +25 -0
- package/dist/client-events.mjs +45 -0
- package/dist/client-events.mjs.map +1 -0
- package/dist/client-stream.d.mts +28 -0
- package/dist/client-stream.mjs +210 -0
- package/dist/client-stream.mjs.map +1 -0
- package/dist/client-transcript.d.mts +41 -0
- package/dist/client-transcript.mjs +233 -0
- package/dist/client-transcript.mjs.map +1 -0
- package/dist/client.d.mts +343 -0
- package/dist/client.mjs +366 -0
- package/dist/client.mjs.map +1 -0
- package/dist/code-review/contracts.d.mts +27 -0
- package/dist/code-review/eve-post-review.d.mts +70 -0
- package/dist/code-review/eve-post-review.mjs +195 -0
- package/dist/code-review/eve-post-review.mjs.map +1 -0
- package/dist/code-review/eve-review-comments.d.mts +75 -0
- package/dist/code-review/eve-review-comments.mjs +193 -0
- package/dist/code-review/eve-review-comments.mjs.map +1 -0
- package/dist/code-review/github-contracts.d.mts +10 -0
- package/dist/code-review/github-reporter.d.mts +46 -0
- package/dist/code-review/github-reporter.mjs +262 -0
- package/dist/code-review/github-reporter.mjs.map +1 -0
- package/dist/code-review/preparation.d.mts +34 -0
- package/dist/code-review/preparation.mjs +23 -0
- package/dist/code-review/preparation.mjs.map +1 -0
- package/dist/code-review/stall.d.mts +17 -0
- package/dist/code-review/stall.mjs +30 -0
- package/dist/code-review/stall.mjs.map +1 -0
- package/dist/code-review.d.mts +7 -0
- package/dist/code-review.mjs +5 -0
- package/dist/communication.d.mts +9 -0
- package/dist/communication.mjs +9 -0
- package/dist/connectors/github.d.mts +26 -0
- package/dist/connectors/github.mjs +55 -0
- package/dist/connectors/github.mjs.map +1 -0
- package/dist/connectors/index.d.mts +35 -3
- package/dist/connectors/index.mjs +3 -52
- package/dist/connectors/index.mjs.map +1 -1
- package/dist/connectors/outbox.d.mts +34 -0
- package/dist/connectors/outbox.mjs +50 -0
- package/dist/connectors/outbox.mjs.map +1 -0
- package/dist/connectors/slack-approval-blocks.mjs +136 -0
- package/dist/connectors/slack-approval-blocks.mjs.map +1 -0
- package/dist/connectors/slack-approvals.d.mts +29 -0
- package/dist/connectors/slack-approvals.mjs +346 -0
- package/dist/connectors/slack-approvals.mjs.map +1 -0
- package/dist/connectors/slack.d.mts +35 -52
- package/dist/connectors/slack.mjs +13 -85
- package/dist/connectors/slack.mjs.map +1 -1
- package/dist/deepsec/eve-tool.d.mts +110 -0
- package/dist/deepsec/eve-tool.mjs +166 -0
- package/dist/deepsec/eve-tool.mjs.map +1 -0
- package/dist/deepsec/finding.d.mts +84 -0
- package/dist/deepsec/finding.mjs +56 -0
- package/dist/deepsec/finding.mjs.map +1 -0
- package/dist/deepsec.d.mts +3 -0
- package/dist/delivery-metrics/metrics.d.mts +93 -0
- package/dist/delivery-metrics/metrics.mjs +200 -0
- package/dist/delivery-metrics/metrics.mjs.map +1 -0
- package/dist/delivery-metrics.d.mts +2 -0
- package/dist/delivery-metrics.mjs +2 -0
- package/dist/dispatch.d.mts +152 -0
- package/dist/dispatch.mjs +199 -0
- package/dist/dispatch.mjs.map +1 -0
- package/dist/errors.d.mts +15 -0
- package/dist/errors.mjs +38 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/eve/index.d.mts +209 -1277
- package/dist/eve/index.mjs +312 -1073
- package/dist/eve/index.mjs.map +1 -1
- package/dist/eve/invoke.d.mts +144 -0
- package/dist/eve/invoke.mjs +254 -0
- package/dist/eve/invoke.mjs.map +1 -0
- package/dist/eve/session-client.d.mts +186 -0
- package/dist/eve/session-client.mjs +245 -0
- package/dist/eve/session-client.mjs.map +1 -0
- package/dist/eve/task-execution.d.mts +380 -0
- package/dist/eve/task-execution.mjs +73 -0
- package/dist/eve/task-execution.mjs.map +1 -0
- package/dist/eve/task-session.d.mts +129 -0
- package/dist/eve/task-session.mjs +245 -0
- package/dist/eve/task-session.mjs.map +1 -0
- package/dist/eve/transcript.d.mts +37 -0
- package/dist/eve/transcript.mjs +69 -0
- package/dist/eve/transcript.mjs.map +1 -0
- package/dist/execution-public.d.mts +8 -0
- package/dist/execution-public.mjs +7 -0
- package/dist/execution.d.mts +183 -0
- package/dist/execution.mjs +223 -0
- package/dist/execution.mjs.map +1 -0
- package/dist/finding-remediation/admission.d.mts +53 -0
- package/dist/finding-remediation/admission.mjs +90 -0
- package/dist/finding-remediation/admission.mjs.map +1 -0
- package/dist/finding-remediation/brief.d.mts +14 -0
- package/dist/finding-remediation/brief.mjs +25 -0
- package/dist/finding-remediation/brief.mjs.map +1 -0
- package/dist/finding-remediation/eve-tool.d.mts +29 -0
- package/dist/finding-remediation/eve-tool.mjs +34 -0
- package/dist/finding-remediation/eve-tool.mjs.map +1 -0
- package/dist/findings/fingerprint.d.mts +14 -0
- package/dist/findings/fingerprint.mjs +28 -0
- package/dist/findings/fingerprint.mjs.map +1 -0
- package/dist/findings.d.mts +7 -0
- package/dist/findings.mjs +6 -0
- package/dist/github-app.d.mts +54 -0
- package/dist/github-app.mjs +121 -0
- package/dist/github-app.mjs.map +1 -0
- package/dist/github-publication.d.mts +199 -0
- package/dist/github-publication.mjs +442 -0
- package/dist/github-publication.mjs.map +1 -0
- package/dist/github-transfer.d.mts +81 -0
- package/dist/github-transfer.mjs +273 -0
- package/dist/github-transfer.mjs.map +1 -0
- package/dist/github.d.mts +446 -2
- package/dist/github.mjs +357 -56
- package/dist/github.mjs.map +1 -1
- package/dist/image-input.d.mts +51 -0
- package/dist/image-input.mjs +63 -0
- package/dist/image-input.mjs.map +1 -0
- 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 +16 -484
- package/dist/index.mjs +15 -522
- package/dist/intake-contracts.d.mts +55 -0
- package/dist/intake-contracts.mjs +33 -0
- package/dist/intake-contracts.mjs.map +1 -0
- package/dist/integrations/deepsec.d.mts +5 -0
- package/dist/integrations/deepsec.mjs +3 -0
- package/dist/integrations/github.d.mts +11 -0
- package/dist/integrations/github.mjs +9 -0
- package/dist/integrations/slack.d.mts +5 -0
- package/dist/integrations/slack.mjs +5 -0
- package/dist/integrations/vercel.d.mts +5 -0
- package/dist/integrations/vercel.mjs +4 -0
- package/dist/merge-resolution/contracts.d.mts +17 -0
- package/dist/merge-resolution/contracts.mjs +19 -0
- package/dist/merge-resolution/contracts.mjs.map +1 -0
- package/dist/merge-resolution/eve-tools.d.mts +47 -0
- package/dist/merge-resolution/eve-tools.mjs +122 -0
- package/dist/merge-resolution/eve-tools.mjs.map +1 -0
- 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/multi-repository-changes/coordination.d.mts +49 -0
- package/dist/multi-repository-changes/coordination.mjs +115 -0
- package/dist/multi-repository-changes/coordination.mjs.map +1 -0
- package/dist/planning/cancellation.mjs +82 -0
- package/dist/planning/cancellation.mjs.map +1 -0
- package/dist/planning/configuration.d.mts +23 -0
- package/dist/planning/configuration.mjs +56 -0
- package/dist/planning/configuration.mjs.map +1 -0
- package/dist/planning/contracts.d.mts +136 -0
- package/dist/planning/contracts.mjs +42 -0
- package/dist/planning/contracts.mjs.map +1 -0
- package/dist/planning/dependencies.mjs +44 -0
- package/dist/planning/dependencies.mjs.map +1 -0
- package/dist/planning/effects.mjs +122 -0
- package/dist/planning/effects.mjs.map +1 -0
- package/dist/planning/reconcile.mjs +279 -0
- package/dist/planning/reconcile.mjs.map +1 -0
- package/dist/planning/runtime.d.mts +27 -0
- package/dist/planning/runtime.mjs +25 -0
- package/dist/planning/runtime.mjs.map +1 -0
- package/dist/planning/state.mjs +92 -0
- package/dist/planning/state.mjs.map +1 -0
- package/dist/planning/turns.mjs +262 -0
- package/dist/planning/turns.mjs.map +1 -0
- package/dist/planning/validation.mjs +106 -0
- package/dist/planning/validation.mjs.map +1 -0
- package/dist/postgres/index.d.mts +48 -1
- package/dist/postgres/index.mjs +45 -3
- package/dist/postgres/index.mjs.map +1 -1
- package/dist/presets/software-development/approval.d.mts +14 -0
- package/dist/presets/software-development/approval.mjs +21 -0
- package/dist/presets/software-development/approval.mjs.map +1 -0
- package/dist/presets/software-development/deepsec-brief.d.mts +8 -0
- package/dist/presets/software-development/deepsec-brief.mjs +14 -0
- package/dist/presets/software-development/deepsec-brief.mjs.map +1 -0
- package/dist/presets/software-development/dispatch.d.mts +56 -0
- package/dist/presets/software-development/dispatch.mjs +67 -0
- package/dist/presets/software-development/dispatch.mjs.map +1 -0
- package/dist/presets/software-development/eve-delegation-policy.d.mts +7 -0
- package/dist/presets/software-development/eve-delegation-policy.mjs +34 -0
- package/dist/presets/software-development/eve-delegation-policy.mjs.map +1 -0
- package/dist/presets/software-development/eve.d.mts +18 -0
- package/dist/presets/software-development/eve.mjs +19 -0
- package/dist/presets/software-development/eve.mjs.map +1 -0
- package/dist/presets/software-development/finish-task-policy.d.mts +7 -0
- package/dist/presets/software-development/finish-task-policy.mjs +9 -0
- package/dist/presets/software-development/finish-task-policy.mjs.map +1 -0
- package/dist/presets/software-development/recovery.d.mts +32 -0
- package/dist/presets/software-development/recovery.mjs +120 -0
- package/dist/presets/software-development/recovery.mjs.map +1 -0
- package/dist/presets/software-development/task-communication.d.mts +24 -0
- package/dist/presets/software-development/task-communication.mjs +157 -0
- package/dist/presets/software-development/task-communication.mjs.map +1 -0
- package/dist/presets/software-development.d.mts +10 -0
- package/dist/presets/software-development.mjs +9 -0
- package/dist/pull-requests/contracts.d.mts +72 -0
- package/dist/pull-requests/eve-fetch.d.mts +57 -0
- package/dist/pull-requests/eve-fetch.mjs +151 -0
- package/dist/pull-requests/eve-fetch.mjs.map +1 -0
- package/dist/pull-requests/github-label-manager.d.mts +27 -0
- package/dist/pull-requests/github-label-manager.mjs +40 -0
- package/dist/pull-requests/github-label-manager.mjs.map +1 -0
- package/dist/pull-requests/github-publisher.d.mts +67 -0
- package/dist/pull-requests/github-publisher.mjs +405 -0
- package/dist/pull-requests/github-publisher.mjs.map +1 -0
- package/dist/pull-requests.d.mts +6 -0
- package/dist/pull-requests.mjs +4 -0
- package/dist/receipts.d.mts +4 -0
- package/dist/receipts.mjs +3 -0
- package/dist/repositories.d.mts +18 -0
- package/dist/repositories.mjs +33 -0
- package/dist/repositories.mjs.map +1 -0
- package/dist/sandbox/command.d.mts +18 -0
- package/dist/sandbox/execution-abort.d.mts +16 -0
- package/dist/sandbox/execution-abort.mjs +49 -0
- package/dist/sandbox/execution-abort.mjs.map +1 -0
- package/dist/sandbox/execution-environment.d.mts +25 -0
- package/dist/sandbox/execution-run-ledger.d.mts +115 -0
- package/dist/sandbox/execution-run-ledger.mjs +209 -0
- package/dist/sandbox/execution-run-ledger.mjs.map +1 -0
- package/dist/sandbox/execution.d.mts +54 -0
- package/dist/sandbox/execution.mjs +228 -0
- package/dist/sandbox/execution.mjs.map +1 -0
- package/dist/sandbox/harness-runner.d.mts +79 -0
- package/dist/sandbox/harness-runner.mjs +162 -0
- package/dist/sandbox/harness-runner.mjs.map +1 -0
- package/dist/sandbox/index.d.mts +10 -0
- package/dist/sandbox/index.mjs +7 -0
- package/dist/sandbox/task-environment.d.mts +50 -0
- package/dist/sandbox/task-environment.mjs +140 -0
- package/dist/sandbox/task-environment.mjs.map +1 -0
- package/dist/sandbox/workspace.d.mts +29 -0
- package/dist/sandbox/workspace.mjs +28 -0
- package/dist/sandbox/workspace.mjs.map +1 -0
- package/dist/schema/agent-route.d.mts +40 -0
- package/dist/schema/agent-route.mjs +33 -0
- package/dist/schema/agent-route.mjs.map +1 -0
- package/dist/schema/answer.d.mts +23 -0
- package/dist/schema/answer.mjs +21 -0
- package/dist/schema/answer.mjs.map +1 -0
- package/dist/schema/budget.d.mts +38 -0
- package/dist/schema/budget.mjs +30 -0
- package/dist/schema/budget.mjs.map +1 -0
- package/dist/schema/catalog.mjs +72 -0
- package/dist/schema/catalog.mjs.map +1 -0
- package/dist/schema/change.d.mts +117 -0
- package/dist/schema/change.mjs +68 -0
- package/dist/schema/change.mjs.map +1 -0
- package/dist/schema/common.d.mts +33 -0
- package/dist/{id-BLIkCVue.mjs → schema/common.mjs} +7 -16
- package/dist/schema/common.mjs.map +1 -0
- package/dist/schema/communication.d.mts +52 -0
- package/dist/{communication-Kd9nU5RP.mjs → schema/communication.mjs} +8 -3
- package/dist/schema/communication.mjs.map +1 -0
- package/dist/schema/execution.d.mts +101 -0
- package/dist/schema/execution.mjs +86 -0
- package/dist/schema/execution.mjs.map +1 -0
- package/dist/schema/factory-config.d.mts +122 -0
- package/dist/schema/factory-config.mjs +90 -0
- package/dist/schema/factory-config.mjs.map +1 -0
- package/dist/schema/finding.d.mts +125 -0
- package/dist/schema/finding.mjs +67 -0
- package/dist/schema/finding.mjs.map +1 -0
- package/dist/schema/id.d.mts +43 -0
- package/dist/schema/id.mjs +32 -0
- package/dist/schema/id.mjs.map +1 -0
- package/dist/schema/outbox.d.mts +240 -0
- package/dist/schema/outbox.mjs +53 -0
- package/dist/schema/outbox.mjs.map +1 -0
- package/dist/schema/question-route.mjs +16 -0
- package/dist/schema/question-route.mjs.map +1 -0
- package/dist/schema/receipt.d.mts +81 -0
- package/dist/schema/receipt.mjs +94 -0
- package/dist/schema/receipt.mjs.map +1 -0
- package/dist/schema/repository.d.mts +30 -0
- package/dist/schema/repository.mjs +29 -0
- package/dist/schema/repository.mjs.map +1 -0
- package/dist/schema/session.d.mts +136 -0
- package/dist/schema/session.mjs +54 -0
- package/dist/schema/session.mjs.map +1 -0
- package/dist/schema/signal.d.mts +105 -0
- package/dist/schema/signal.mjs +81 -0
- package/dist/schema/signal.mjs.map +1 -0
- 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/stored-session.mjs +10 -0
- package/dist/schema/stored-session.mjs.map +1 -0
- package/dist/schema/stored-task.mjs +20 -0
- package/dist/schema/stored-task.mjs.map +1 -0
- package/dist/schema/task-effect.d.mts +39 -0
- package/dist/schema/task-effect.mjs +29 -0
- package/dist/schema/task-effect.mjs.map +1 -0
- package/dist/schema/task-graph.d.mts +3173 -0
- package/dist/schema/task-graph.mjs +97 -0
- package/dist/schema/task-graph.mjs.map +1 -0
- package/dist/schema/task-message.d.mts +254 -0
- package/dist/schema/task-message.mjs +189 -0
- package/dist/schema/task-message.mjs.map +1 -0
- package/dist/schema/task.d.mts +273 -0
- package/dist/schema/task.mjs +200 -0
- package/dist/schema/task.mjs.map +1 -0
- package/dist/schema/transcript.d.mts +176 -0
- package/dist/schema/transcript.mjs +87 -0
- package/dist/schema/transcript.mjs.map +1 -0
- package/dist/schema/transitions.d.mts +50 -0
- package/dist/schema/transitions.mjs +303 -0
- package/dist/schema/transitions.mjs.map +1 -0
- package/dist/schema/work.d.mts +150 -0
- package/dist/schema/work.mjs +102 -0
- package/dist/schema/work.mjs.map +1 -0
- 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.d.mts +20 -0
- package/dist/signal-triage.mjs +85 -0
- package/dist/signal-triage.mjs.map +1 -0
- package/dist/signals.d.mts +20 -0
- package/dist/signals.mjs +19 -0
- package/dist/signals.mjs.map +1 -0
- package/dist/stall.d.mts +41 -0
- package/dist/stall.mjs +83 -0
- package/dist/stall.mjs.map +1 -0
- package/dist/storage.d.mts +4 -0
- package/dist/storage.mjs +4 -0
- package/dist/store/driver.d.mts +65 -0
- package/dist/store/driver.mjs +42 -0
- package/dist/store/driver.mjs.map +1 -0
- package/dist/store/engine.d.mts +683 -0
- package/dist/store/engine.mjs +2309 -0
- package/dist/store/engine.mjs.map +1 -0
- package/dist/store/memory.d.mts +24 -0
- package/dist/store/memory.mjs +69 -0
- package/dist/store/memory.mjs.map +1 -0
- 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-effects.d.mts +38 -0
- package/dist/store/task-effects.mjs +118 -0
- package/dist/store/task-effects.mjs.map +1 -0
- package/dist/store/task-graphs.d.mts +60 -0
- package/dist/store/task-graphs.mjs +212 -0
- package/dist/store/task-graphs.mjs.map +1 -0
- package/dist/store/task-messages.d.mts +136 -0
- package/dist/store/task-messages.mjs +475 -0
- package/dist/store/task-messages.mjs.map +1 -0
- package/dist/store/task-work.d.mts +180 -0
- package/dist/store/task-work.mjs +137 -0
- package/dist/store/task-work.mjs.map +1 -0
- package/dist/store/work-notifications.d.mts +19 -0
- package/dist/store/work-notifications.mjs +48 -0
- package/dist/store/work-notifications.mjs.map +1 -0
- package/dist/sweep.d.mts +79 -0
- package/dist/sweep.mjs +171 -0
- package/dist/sweep.mjs.map +1 -0
- package/dist/task-communication.d.mts +35 -0
- package/dist/task-communication.mjs +47 -0
- package/dist/task-communication.mjs.map +1 -0
- package/dist/task-graph-view.d.mts +267 -0
- package/dist/task-graph-view.mjs +17 -0
- package/dist/task-graph-view.mjs.map +1 -0
- package/dist/task-message-processing.d.mts +60 -0
- package/dist/task-message-processing.mjs +200 -0
- package/dist/task-message-processing.mjs.map +1 -0
- package/dist/task-protocols.d.mts +80 -0
- package/dist/task-protocols.mjs +47 -0
- package/dist/task-protocols.mjs.map +1 -0
- package/dist/tasks.d.mts +17 -0
- package/dist/tasks.mjs +17 -0
- package/dist/vercel-git.d.mts +53 -0
- package/dist/vercel-git.mjs +344 -0
- package/dist/vercel-git.mjs.map +1 -0
- 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 +34 -2
- package/dist/vercel.mjs +49 -3
- package/dist/vercel.mjs.map +1 -1
- package/dist/work-triage/eve-tool.d.mts +29 -0
- package/dist/work-triage/eve-tool.mjs +32 -0
- package/dist/work-triage/eve-tool.mjs.map +1 -0
- package/dist/work-triage/find-existing-work.d.mts +32 -0
- package/dist/work-triage/find-existing-work.mjs +31 -0
- package/dist/work-triage/find-existing-work.mjs.map +1 -0
- package/dist/work-triage/github.d.mts +15 -0
- package/dist/work-triage/github.mjs +14 -0
- package/dist/work-triage/github.mjs.map +1 -0
- package/dist/work-triage.d.mts +4 -0
- package/dist/work-triage.mjs +3 -0
- package/dist/workflows.d.mts +146 -0
- package/dist/workflows.mjs +139 -0
- package/dist/workflows.mjs.map +1 -0
- package/dist/workspace-diff-assembly.d.mts +33 -0
- package/dist/workspace-diff-assembly.mjs +95 -0
- package/dist/workspace-diff-assembly.mjs.map +1 -0
- package/dist/workspace-diff.d.mts +90 -0
- package/dist/workspace-diff.mjs +61 -0
- package/dist/workspace-diff.mjs.map +1 -0
- 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/dist/workspace-snapshot.d.mts +26 -0
- package/dist/workspace-snapshot.mjs +80 -0
- package/dist/workspace-snapshot.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 +142 -46
- package/dist/approval-JX2Px9IC.mjs +0 -132
- package/dist/approval-JX2Px9IC.mjs.map +0 -1
- package/dist/budget-DzSiIA4t.mjs +0 -42
- package/dist/budget-DzSiIA4t.mjs.map +0 -1
- package/dist/change-D2U6v7Ek.mjs +0 -554
- package/dist/change-D2U6v7Ek.mjs.map +0 -1
- package/dist/communication-Kd9nU5RP.mjs.map +0 -1
- package/dist/coordination-Cq08hb1l.mjs +0 -76
- package/dist/coordination-Cq08hb1l.mjs.map +0 -1
- package/dist/dispatch-BaZS3PT8.d.mts +0 -76
- package/dist/driver-BC0Mt5KG.d.mts +0 -34
- package/dist/driver-Bbd25rNM.mjs +0 -23
- package/dist/driver-Bbd25rNM.mjs.map +0 -1
- package/dist/engine-DFHU2Pe-.d.mts +0 -781
- package/dist/engine-WaUM2cGA.mjs +0 -1068
- package/dist/engine-WaUM2cGA.mjs.map +0 -1
- package/dist/factory-config-BoRXOw43.mjs +0 -50
- package/dist/factory-config-BoRXOw43.mjs.map +0 -1
- package/dist/factory-config-D2yU3G0c.d.mts +0 -58
- package/dist/github-C5HNBN6p.d.mts +0 -279
- package/dist/id-BLIkCVue.mjs.map +0 -1
- package/dist/index-Bw6hggoH.d.mts +0 -40
- package/dist/index.mjs.map +0 -1
- package/dist/task-CcOyOJ3k.mjs +0 -154
- package/dist/task-CcOyOJ3k.mjs.map +0 -1
package/dist/eve/index.mjs
CHANGED
|
@@ -1,947 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
import { changeIdSchema, repositoryIdsSchema, signalIdSchema, taskIdSchema } from "../schema/id.mjs";
|
|
2
|
+
import { taskWork } from "../schema/work.mjs";
|
|
3
|
+
import { taskQuestionSchema } from "../schema/task.mjs";
|
|
4
|
+
import { conversationBinding, conversationBindingForSession, conversationHeaders, factoryReplyAddressAttribute, factoryReplyAddressHeader, factoryReplyChannelAttribute, factoryReplyChannelHeader, factoryRepositoryIdsAttribute, factoryRepositoryIdsHeader, factoryTaskAttemptAttribute, factoryTaskAttemptHeader, factoryTaskAttribute, factoryTaskHeader, hasFactoryTaskBinding, optionalTaskIdForSession, replyToForSession, repositoryIdsForSession, requireTaskIdForSession, taskBindingForSession, taskHeaders, taskSessionAuth, withFactoryTask, withOptionalFactoryTask } from "./task-session.mjs";
|
|
5
|
+
import { requireTaskExecution } from "./task-execution.mjs";
|
|
6
|
+
import { receiptKindSchema, receiptStateSchema } from "../schema/receipt.mjs";
|
|
7
|
+
import { createEveTranscriptMaterializer } from "./transcript.mjs";
|
|
8
|
+
import { cancelEveSession, forwardEveSessionRequest, readCompletedEveTurn, startEveSession } from "./session-client.mjs";
|
|
9
|
+
import { createDelegateAgentTool, createInvokeAgentTool } from "./invoke.mjs";
|
|
7
10
|
import { z } from "zod";
|
|
8
|
-
import { createHash } from "node:crypto";
|
|
9
11
|
import { defineHook } from "eve/hooks";
|
|
10
12
|
import { defineTool } from "eve/tools";
|
|
11
|
-
//#region src/eve/conflict.ts
|
|
12
|
-
const gitShaSchema = z.string().regex(/^[0-9a-f]{40}$/);
|
|
13
|
-
const gitBranchSchema = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._/-]*$/).refine((value) => !value.includes(".."));
|
|
14
|
-
const mergeConflictPayloadSchema = z.strictObject({
|
|
15
|
-
repository: z.string().regex(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/),
|
|
16
|
-
prNumber: z.number().int().positive(),
|
|
17
|
-
prUrl: z.url(),
|
|
18
|
-
headBranch: gitBranchSchema.refine((value) => value.startsWith("factory/")),
|
|
19
|
-
headSha: gitShaSchema,
|
|
20
|
-
baseBranch: gitBranchSchema,
|
|
21
|
-
baseSha: gitShaSchema
|
|
22
|
-
});
|
|
23
|
-
function mergeConflictTools(stores, options) {
|
|
24
|
-
const repoPath = options.repoPath ?? "repo";
|
|
25
|
-
async function context(taskId) {
|
|
26
|
-
const task = await stores.tasks.get(taskId);
|
|
27
|
-
if (task === null) throw new Error(`task ${taskId} does not exist`);
|
|
28
|
-
if (task.kind !== "merge_conflict") throw new Error(`task ${taskId} is ${task.kind}, not merge_conflict`);
|
|
29
|
-
if (task.state !== "running") throw new Error(`task ${taskId} is ${task.state}, not running`);
|
|
30
|
-
if (task.origin.signalId === void 0) throw new Error(`task ${taskId} has no conflict signal`);
|
|
31
|
-
const signal = await stores.signals.get(task.origin.signalId);
|
|
32
|
-
if (signal === null || signal.source !== "github_merge_conflict") throw new Error(`task ${taskId} has no GitHub merge conflict signal`);
|
|
33
|
-
return {
|
|
34
|
-
task,
|
|
35
|
-
conflict: mergeConflictPayloadSchema.parse(signal.payload)
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
return {
|
|
39
|
-
fetch_merge_conflict: defineTool({
|
|
40
|
-
description: "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.",
|
|
41
|
-
inputSchema: z.object({ taskId: taskIdSchema }),
|
|
42
|
-
async execute({ taskId }, ctx) {
|
|
43
|
-
const { conflict } = await context(taskId);
|
|
44
|
-
const sandbox = await ctx.getSandbox();
|
|
45
|
-
const git = `git -C ${repoPath}`;
|
|
46
|
-
const headRef = `refs/remotes/origin/conflict-pr-${conflict.prNumber}`;
|
|
47
|
-
const baseRef = `refs/remotes/origin/conflict-base-${conflict.prNumber}`;
|
|
48
|
-
const fetch = await options.withCredential(sandbox, conflict.repository, `${git} fetch --depth 100 origin +refs/pull/${conflict.prNumber}/head:${headRef} +refs/heads/${conflict.baseBranch}:${baseRef}`);
|
|
49
|
-
if (typeof fetch.exitCode === "number" && fetch.exitCode !== 0) throw new Error(`could not fetch pull request: ${(fetch.stderr ?? "").trim()}`);
|
|
50
|
-
const [headSha, baseSha] = (await sandbox.run({ command: `${git} rev-parse ${headRef} && ${git} rev-parse ${baseRef}` })).stdout.trim().split("\n");
|
|
51
|
-
if (headSha !== conflict.headSha || baseSha !== conflict.baseSha) throw new Error("pull request or base changed while conflict resolution was starting; retry from a new task");
|
|
52
|
-
await sandbox.run({ command: `${git} checkout -B conflict-pr ${headRef}` });
|
|
53
|
-
const merge = await sandbox.run({ command: `${git} merge --no-commit --no-ff ${baseRef}` });
|
|
54
|
-
const conflictedFiles = (await sandbox.run({ command: `${git} diff --name-only --diff-filter=U` })).stdout.trim().split("\n").filter(Boolean);
|
|
55
|
-
if (conflictedFiles.length === 0) throw new Error(typeof merge.exitCode === "number" && merge.exitCode !== 0 ? `merge failed without file conflicts: ${(merge.stderr ?? "").trim()}` : "pull request no longer has merge conflicts");
|
|
56
|
-
return {
|
|
57
|
-
...conflict,
|
|
58
|
-
conflictedFiles
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
}),
|
|
62
|
-
publish_merge_resolution: defineTool({
|
|
63
|
-
description: "Publish a tested, committed conflict resolution to the existing pull request branch. Fails if the remote branch changed since this task started.",
|
|
64
|
-
inputSchema: z.object({
|
|
65
|
-
taskId: taskIdSchema,
|
|
66
|
-
summary: z.string().min(1).max(2e3)
|
|
67
|
-
}),
|
|
68
|
-
async execute({ taskId, summary }, ctx) {
|
|
69
|
-
const { conflict } = await context(taskId);
|
|
70
|
-
const sandbox = await ctx.getSandbox();
|
|
71
|
-
const git = `git -C ${repoPath}`;
|
|
72
|
-
if ((await sandbox.run({ command: `${git} status --porcelain` })).stdout.trim() !== "") throw new Error("commit the conflict resolution before publishing");
|
|
73
|
-
if ((await sandbox.run({ command: `${git} diff --name-only --diff-filter=U` })).stdout.trim() !== "") throw new Error("merge conflicts remain unresolved");
|
|
74
|
-
const push = await options.withCredential(sandbox, conflict.repository, `${git} push --force-with-lease=refs/heads/${conflict.headBranch}:${conflict.headSha} origin HEAD:refs/heads/${conflict.headBranch}`);
|
|
75
|
-
if (typeof push.exitCode === "number" && push.exitCode !== 0) throw new Error(`could not update pull request branch: ${(push.stderr ?? "").trim()}`);
|
|
76
|
-
await stores.tasks.transition(taskId, "verifying", { reason: summary });
|
|
77
|
-
await stores.tasks.transition(taskId, "succeeded", { reason: summary });
|
|
78
|
-
return {
|
|
79
|
-
updated: true,
|
|
80
|
-
prNumber: conflict.prNumber,
|
|
81
|
-
prUrl: conflict.prUrl
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
})
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
//#endregion
|
|
88
|
-
//#region src/eve/finding.ts
|
|
89
|
-
function isLostRaceError(error) {
|
|
90
|
-
return error instanceof Error && (error.name === "VersionConflictError" || error.name === "InvalidTransitionError");
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Turn one finding into a signal and a fix task. The fingerprint is the
|
|
94
|
-
* signal's delivery id, so the same problem reported twice is a no-op. The
|
|
95
|
-
* signal is accepted on the reporter's own verification (a human gate follows
|
|
96
|
-
* when the approval policy says so), and the task carries the approval
|
|
97
|
-
* decision from the moment it exists. Shared by record_finding and scanner
|
|
98
|
-
* tools that record on the agent's behalf.
|
|
99
|
-
*/
|
|
100
|
-
async function recordFinding(stores, repositoryId, finding, options = {}) {
|
|
101
|
-
const source = options.source ?? "security_scan";
|
|
102
|
-
const policy = options.approval ?? defaultApprovalPolicy;
|
|
103
|
-
if (await stores.repositories.get(repositoryId) === null) return {
|
|
104
|
-
recorded: false,
|
|
105
|
-
reason: `repository ${repositoryId} does not exist`
|
|
106
|
-
};
|
|
107
|
-
const fingerprint = findingFingerprint(repositoryId, finding);
|
|
108
|
-
const signal = await stores.signals.create({
|
|
109
|
-
repositoryId,
|
|
110
|
-
source,
|
|
111
|
-
deliveryId: fingerprint,
|
|
112
|
-
replyTo: {
|
|
113
|
-
channel: "finding",
|
|
114
|
-
address: fingerprint
|
|
115
|
-
},
|
|
116
|
-
payload: finding
|
|
117
|
-
});
|
|
118
|
-
if (signal.outcome !== "pending") return {
|
|
119
|
-
recorded: false,
|
|
120
|
-
duplicate: true,
|
|
121
|
-
signalId: signal.id,
|
|
122
|
-
reason: `already recorded as ${signal.id}`
|
|
123
|
-
};
|
|
124
|
-
try {
|
|
125
|
-
await stores.signals.decide(signal.id, "accepted", {
|
|
126
|
-
detail: finding.priority,
|
|
127
|
-
reason: `${finding.tool ?? "scanner"} reported it, ${finding.verification.level} by ${finding.verification.method}`
|
|
128
|
-
});
|
|
129
|
-
} catch (error) {
|
|
130
|
-
if (isLostRaceError(error)) {
|
|
131
|
-
const current = await stores.signals.get(signal.id);
|
|
132
|
-
if (current !== null && current.outcome !== "pending") return {
|
|
133
|
-
recorded: false,
|
|
134
|
-
duplicate: true,
|
|
135
|
-
signalId: signal.id,
|
|
136
|
-
reason: `already recorded as ${signal.id}`
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
throw error;
|
|
140
|
-
}
|
|
141
|
-
const approval = requiresApproval(policy, {
|
|
142
|
-
source,
|
|
143
|
-
kind: "code_change",
|
|
144
|
-
priority: finding.priority
|
|
145
|
-
});
|
|
146
|
-
const task = await stores.tasks.create({
|
|
147
|
-
repositoryIds: [repositoryId],
|
|
148
|
-
kind: "code_change",
|
|
149
|
-
origin: { signalId: signal.id },
|
|
150
|
-
replyTo: signal.replyTo,
|
|
151
|
-
...approval ? { approval: "required" } : {}
|
|
152
|
-
});
|
|
153
|
-
return {
|
|
154
|
-
recorded: true,
|
|
155
|
-
signalId: signal.id,
|
|
156
|
-
taskId: task.id,
|
|
157
|
-
approval: approval ? "required" : "not_required"
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* The scanner's outcome tool: turns one finding into a signal and a fix task
|
|
162
|
-
* in a single deterministic step. The finding's fingerprint is the signal's
|
|
163
|
-
* delivery id, so reporting the same problem twice is a no-op. The signal is
|
|
164
|
-
* accepted on the reporter's own verification (a human gate follows when the
|
|
165
|
-
* approval policy says so), and the task carries the approval decision from
|
|
166
|
-
* the moment it exists.
|
|
167
|
-
*/
|
|
168
|
-
function recordFindingTool(stores, options = {}) {
|
|
169
|
-
return defineTool({
|
|
170
|
-
description: "Record one problem you found in the repository. Creates a fix task (which may wait for human approval) unless the same finding was already recorded. Report every finding separately; never batch.",
|
|
171
|
-
inputSchema: z.object({
|
|
172
|
-
repositoryId: repositoryIdSchema,
|
|
173
|
-
finding: findingSchema
|
|
174
|
-
}),
|
|
175
|
-
execute({ repositoryId, finding }) {
|
|
176
|
-
return recordFinding(stores, repositoryId, finding, options);
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
//#endregion
|
|
181
|
-
//#region src/eve/deepsec.ts
|
|
182
|
-
/** deepsec's exported finding shape, as far as we depend on it. */
|
|
183
|
-
const deepsecFindingSchema = z.looseObject({
|
|
184
|
-
title: z.string().min(1),
|
|
185
|
-
description: z.string().min(1),
|
|
186
|
-
severity: z.string().min(1),
|
|
187
|
-
metadata: z.looseObject({
|
|
188
|
-
filePath: z.string().min(1).optional(),
|
|
189
|
-
lineNumbers: z.array(z.number().int().min(1)).optional(),
|
|
190
|
-
vulnSlug: z.string().min(1).optional(),
|
|
191
|
-
confidence: z.string().optional(),
|
|
192
|
-
runId: z.string().optional()
|
|
193
|
-
}).optional()
|
|
194
|
-
});
|
|
195
|
-
const deepsecExportSchema = z.union([z.array(deepsecFindingSchema), z.looseObject({ findings: z.array(deepsecFindingSchema) }).transform((value) => value.findings)]);
|
|
196
|
-
const priorityBySeverity = {
|
|
197
|
-
CRITICAL: "P0",
|
|
198
|
-
HIGH: "P1",
|
|
199
|
-
MEDIUM: "P2",
|
|
200
|
-
HIGH_BUG: "P2",
|
|
201
|
-
BUG: "P3",
|
|
202
|
-
LOW: "P3"
|
|
203
|
-
};
|
|
204
|
-
/** Convert one deepsec finding into the factory's Finding. Pure; exported for tests. */
|
|
205
|
-
function findingFromDeepsec(raw) {
|
|
206
|
-
const meta = raw.metadata ?? {};
|
|
207
|
-
const confidence = (meta.confidence ?? "").toLowerCase();
|
|
208
|
-
const line = meta.lineNumbers?.[0];
|
|
209
|
-
const endLine = meta.lineNumbers === void 0 ? void 0 : Math.max(...meta.lineNumbers);
|
|
210
|
-
return findingSchema.parse({
|
|
211
|
-
title: raw.title.replace(/^\[[A-Z_]+\]\s*/u, "").slice(0, 200),
|
|
212
|
-
body: raw.description.slice(0, 8e3),
|
|
213
|
-
...meta.filePath === void 0 ? {} : { path: meta.filePath.slice(0, 500) },
|
|
214
|
-
...line === void 0 ? {} : { line },
|
|
215
|
-
...endLine === void 0 || endLine === line ? {} : { endLine },
|
|
216
|
-
priority: priorityBySeverity[raw.severity.toUpperCase()] ?? "P3",
|
|
217
|
-
category: "security",
|
|
218
|
-
verification: {
|
|
219
|
-
level: confidence === "high" ? "supported" : "unverified",
|
|
220
|
-
method: "static_analysis",
|
|
221
|
-
summary: `deepsec ${meta.vulnSlug ?? "finding"} with ${confidence || "unknown"} confidence${meta.runId === void 0 ? "" : ` (run ${meta.runId})`}.`,
|
|
222
|
-
limitations: "AI-assisted static analysis of the checkout; not reproduced at runtime."
|
|
223
|
-
},
|
|
224
|
-
tool: "deepsec",
|
|
225
|
-
...meta.vulnSlug === void 0 ? {} : { ruleId: meta.vulnSlug }
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
const findingsPath = "/tmp/deepsec-findings.json";
|
|
229
|
-
/** Parse "N file(s) with candidates" out of deepsec scan output; null when absent. */
|
|
230
|
-
function candidateCount(output) {
|
|
231
|
-
const match = /(\d+)\s+file\(s\) with candidates/u.exec(output);
|
|
232
|
-
return match?.[1] === void 0 ? null : Number(match[1]);
|
|
233
|
-
}
|
|
234
|
-
/** The last few meaningful lines of a command's output, ANSI stripped, for a receipt. */
|
|
235
|
-
function tail(output, lines = 4) {
|
|
236
|
-
return output.replaceAll(/\u001b\[[0-9;]*m/gu, "").split("\n").map((line) => line.trim()).filter((line) => line !== "").slice(-lines).join(" | ").slice(0, 600);
|
|
237
|
-
}
|
|
238
|
-
/**
|
|
239
|
-
* Runs deepsec (vercel-labs/deepsec) against the sandbox checkout and records
|
|
240
|
-
* what it finds through the same path as record_finding, so every deepsec
|
|
241
|
-
* finding becomes a fix task subject to the approval policy. Trusted code
|
|
242
|
-
* owns the command sequence and the credential; the agent chooses when to
|
|
243
|
-
* scan and reports the outcome.
|
|
244
|
-
*/
|
|
245
|
-
function deepsecScanTool(stores, options) {
|
|
246
|
-
const repoPath = options.repoPath ?? "repo";
|
|
247
|
-
const workspace = options.workspacePath ?? "deepsec";
|
|
248
|
-
const version = options.version ?? "2.3.8";
|
|
249
|
-
const agent = options.agent ?? "claude";
|
|
250
|
-
const model = options.model ?? "claude-sonnet-4-6";
|
|
251
|
-
const thinkingLevel = options.thinkingLevel ?? "medium";
|
|
252
|
-
const concurrency = options.concurrency ?? 4;
|
|
253
|
-
const prefix = options.commandPrefix === void 0 ? [] : [options.commandPrefix];
|
|
254
|
-
const filesPerRun = options.filesPerRun ?? 40;
|
|
255
|
-
return defineTool({
|
|
256
|
-
description: "Run deepsec, the AI-powered vulnerability scanner, over the repository checkout and record every finding it reports as a fix task. Takes minutes. Call once per scan; then summarize the result.",
|
|
257
|
-
inputSchema: z.object({
|
|
258
|
-
repositoryId: repositoryIdSchema,
|
|
259
|
-
limit: z.number().int().min(1).max(500).optional().describe("Candidate files to investigate this run (default 40; deepsec resumes where it left off next time).")
|
|
260
|
-
}),
|
|
261
|
-
async execute({ repositoryId, limit }, ctx) {
|
|
262
|
-
const repository = await stores.repositories.get(repositoryId);
|
|
263
|
-
if (repository === null) return {
|
|
264
|
-
scanned: false,
|
|
265
|
-
reason: `repository ${repositoryId} does not exist`
|
|
266
|
-
};
|
|
267
|
-
const sandbox = await ctx.getSandbox();
|
|
268
|
-
const projectId = repository.slug.replace(/[^a-z0-9]+/gu, "-");
|
|
269
|
-
const startedAt = Date.now();
|
|
270
|
-
const phase = async (state, reason) => {
|
|
271
|
-
await stores.receipts.append({
|
|
272
|
-
repositoryIds: [repositoryId],
|
|
273
|
-
kind: "deepsec",
|
|
274
|
-
state,
|
|
275
|
-
phase: "security_scan",
|
|
276
|
-
reason: `${reason} (${Math.round((Date.now() - startedAt) / 1e3)}s in)`
|
|
277
|
-
});
|
|
278
|
-
};
|
|
279
|
-
await phase("started", `deepsec ${version} on ${repository.slug}, ${agent}/${model}`);
|
|
280
|
-
const prepare = await sandbox.run({ command: [
|
|
281
|
-
...prefix,
|
|
282
|
-
"set -e",
|
|
283
|
-
`if [ ! -f ${workspace}/deepsec.config.ts ]; then`,
|
|
284
|
-
` npx -y deepsec@${version} init ./${workspace} ./${repoPath} --id ${projectId} --scaffold-only --headless --yes --skip-install`,
|
|
285
|
-
` (cd ${workspace} && npm install --no-audit --no-fund --loglevel=error)`,
|
|
286
|
-
"fi",
|
|
287
|
-
`cd ${workspace}`,
|
|
288
|
-
`./node_modules/.bin/deepsec scan --project-id ${projectId}`
|
|
289
|
-
].join("\n") });
|
|
290
|
-
if (typeof prepare.exitCode === "number" && prepare.exitCode !== 0) {
|
|
291
|
-
const detail = (prepare.stderr ?? prepare.stdout).trim().slice(-1500);
|
|
292
|
-
await phase("failed", `scan phase failed: ${detail.slice(-300)}`);
|
|
293
|
-
return {
|
|
294
|
-
scanned: false,
|
|
295
|
-
reason: `deepsec scan failed: ${detail}`
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
const candidates = candidateCount(prepare.stdout);
|
|
299
|
-
await phase("scanned", `${candidates === null ? "scan output" : `${candidates} files with candidates`}: ${tail(`${prepare.stdout}\n${prepare.stderr ?? ""}`)}`);
|
|
300
|
-
const investigate = await options.withModelCredential(sandbox, [
|
|
301
|
-
...prefix,
|
|
302
|
-
"set -e",
|
|
303
|
-
`cd ${workspace}`,
|
|
304
|
-
`./node_modules/.bin/deepsec process --project-id ${projectId} --agent ${agent} --model ${model} --thinking-level ${thinkingLevel} --concurrency ${concurrency} --limit ${limit ?? filesPerRun}`,
|
|
305
|
-
`./node_modules/.bin/deepsec export --project-id ${projectId} --format json --out ${findingsPath}`
|
|
306
|
-
].join("\n"));
|
|
307
|
-
if (typeof investigate.exitCode === "number" && investigate.exitCode !== 0) {
|
|
308
|
-
const detail = (investigate.stderr ?? investigate.stdout).trim().slice(-1500);
|
|
309
|
-
await phase("failed", `process phase failed: ${detail.slice(-300)}`);
|
|
310
|
-
return {
|
|
311
|
-
scanned: false,
|
|
312
|
-
reason: `deepsec process failed: ${detail}`
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
const exported = await sandbox.run({ command: `cat ${findingsPath}` });
|
|
316
|
-
let parsed;
|
|
317
|
-
try {
|
|
318
|
-
parsed = JSON.parse(exported.stdout);
|
|
319
|
-
} catch {
|
|
320
|
-
return {
|
|
321
|
-
scanned: false,
|
|
322
|
-
reason: "deepsec export was not valid JSON"
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
const raw = deepsecExportSchema.safeParse(parsed);
|
|
326
|
-
if (!raw.success) return {
|
|
327
|
-
scanned: false,
|
|
328
|
-
reason: `deepsec export had an unexpected shape: ${raw.error.issues[0]?.message ?? "unknown"}`
|
|
329
|
-
};
|
|
330
|
-
const results = [];
|
|
331
|
-
for (const entry of raw.data) {
|
|
332
|
-
const finding = findingFromDeepsec(entry);
|
|
333
|
-
results.push({
|
|
334
|
-
finding,
|
|
335
|
-
result: await recordFinding(stores, repositoryId, finding, options)
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
const recorded = results.filter(({ result }) => result.recorded);
|
|
339
|
-
await phase("completed", `${results.length} findings, ${recorded.length} new. process output: ${tail(`${investigate.stdout}\n${investigate.stderr ?? ""}`, 6)}`);
|
|
340
|
-
return {
|
|
341
|
-
scanned: true,
|
|
342
|
-
tool: "deepsec",
|
|
343
|
-
found: results.length,
|
|
344
|
-
recorded: recorded.length,
|
|
345
|
-
duplicates: results.length - recorded.length,
|
|
346
|
-
awaitingApproval: recorded.filter(({ result }) => result.recorded && result.approval === "required").length,
|
|
347
|
-
findings: results.map(({ finding, result }) => ({
|
|
348
|
-
title: finding.title,
|
|
349
|
-
priority: finding.priority,
|
|
350
|
-
path: finding.path,
|
|
351
|
-
line: finding.line,
|
|
352
|
-
...result.recorded ? {
|
|
353
|
-
taskId: result.taskId,
|
|
354
|
-
approval: result.approval
|
|
355
|
-
} : { duplicate: true }
|
|
356
|
-
}))
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
//#endregion
|
|
362
|
-
//#region src/eve/fetch.ts
|
|
363
|
-
/**
|
|
364
|
-
* The verifier's way in to the change under review. The sandbox has no
|
|
365
|
-
* GitHub credential while the agent runs, so trusted code does the one fetch
|
|
366
|
-
* the agent cannot: the pull request number and repository come from the
|
|
367
|
-
* change record, never from the agent, and the credential is gone again
|
|
368
|
-
* before control returns. Mirrors how publication reads commits out of the
|
|
369
|
-
* sandbox instead of letting the agent push.
|
|
370
|
-
*/
|
|
371
|
-
function fetchPullRequestTool(stores, options) {
|
|
372
|
-
const repoPath = options.repoPath ?? "repo";
|
|
373
|
-
const depth = options.depth ?? 50;
|
|
374
|
-
return defineTool({
|
|
375
|
-
description: "Fetch the pull request and the repository's current default branch, then check out the pull request as local branch `pr`. Call this after get_task, before inspecting or testing the change. Returns an exact diff command based on their merge base; use it instead of origin/HEAD.",
|
|
376
|
-
inputSchema: z.object({ changeId: changeIdSchema.describe("The Change linked to your Task, returned by get_task.") }),
|
|
377
|
-
async execute({ changeId }, ctx) {
|
|
378
|
-
const boundTaskId = taskIdSchema.safeParse(ctx.session?.auth.initiator?.attributes.factoryTaskId);
|
|
379
|
-
if (boundTaskId.success) {
|
|
380
|
-
const task = await stores.tasks.get(boundTaskId.data);
|
|
381
|
-
if (task === null) return {
|
|
382
|
-
fetched: false,
|
|
383
|
-
reason: `task ${boundTaskId.data} does not exist`
|
|
384
|
-
};
|
|
385
|
-
if (task.changeId !== void 0 && task.changeId !== changeId) return {
|
|
386
|
-
fetched: false,
|
|
387
|
-
reason: `change ${changeId} is not linked to task ${boundTaskId.data}`
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
const change = await stores.changes.get(changeId);
|
|
391
|
-
if (change === null) return {
|
|
392
|
-
fetched: false,
|
|
393
|
-
reason: `change ${changeId} does not exist`
|
|
394
|
-
};
|
|
395
|
-
if (change.state !== "verifying" && change.state !== "ready") return {
|
|
396
|
-
fetched: false,
|
|
397
|
-
reason: `change is ${change.state}, not open for review; nothing to fetch`
|
|
398
|
-
};
|
|
399
|
-
const repository = await stores.repositories.get(change.repositoryId);
|
|
400
|
-
if (repository === null) return {
|
|
401
|
-
fetched: false,
|
|
402
|
-
reason: `repository ${change.repositoryId} does not exist`
|
|
403
|
-
};
|
|
404
|
-
const sandbox = await ctx.getSandbox();
|
|
405
|
-
const tracking = `refs/remotes/origin/pr-${change.prNumber}`;
|
|
406
|
-
const baseTracking = `refs/remotes/origin/${repository.defaultBranch}`;
|
|
407
|
-
const git = `git -C ${repoPath}`;
|
|
408
|
-
const result = await options.withCredential(sandbox, repository.slug, `${git} fetch --depth ${depth} origin +refs/heads/${repository.defaultBranch}:${baseTracking} +refs/pull/${change.prNumber}/head:${tracking} && ${git} checkout -B pr ${tracking}`);
|
|
409
|
-
if (typeof result.exitCode === "number" && result.exitCode !== 0) return {
|
|
410
|
-
fetched: false,
|
|
411
|
-
reason: `git fetch of pull request ${change.prNumber} failed: ${(result.stderr ?? "").trim()}`
|
|
412
|
-
};
|
|
413
|
-
const head = await sandbox.run({ command: `${git} rev-parse HEAD` });
|
|
414
|
-
const base = await sandbox.run({ command: `${git} rev-parse ${baseTracking}` });
|
|
415
|
-
const mergeBase = await sandbox.run({ command: `${git} merge-base ${baseTracking} pr` });
|
|
416
|
-
if (typeof mergeBase.exitCode === "number" && mergeBase.exitCode !== 0) return {
|
|
417
|
-
fetched: false,
|
|
418
|
-
reason: `could not determine the pull request merge base: ${(mergeBase.stderr ?? "").trim()}`
|
|
419
|
-
};
|
|
420
|
-
const mergeBaseSha = mergeBase.stdout.trim();
|
|
421
|
-
return {
|
|
422
|
-
fetched: true,
|
|
423
|
-
prNumber: change.prNumber,
|
|
424
|
-
prUrl: change.prUrl,
|
|
425
|
-
branch: "pr",
|
|
426
|
-
headSha: head.stdout.trim(),
|
|
427
|
-
baseBranch: repository.defaultBranch,
|
|
428
|
-
baseSha: base.stdout.trim(),
|
|
429
|
-
mergeBaseSha,
|
|
430
|
-
diffCommand: `${git} diff ${mergeBaseSha}..pr`
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
//#endregion
|
|
436
|
-
//#region src/eve/invoke.ts
|
|
437
|
-
/**
|
|
438
|
-
* A tool that lets one agent hand work to another, NMA-style: the caller
|
|
439
|
-
* decides to invoke; trusted code owns the how. The handoff is recorded as a
|
|
440
|
-
* factory task (single-winner dispatch, receipts), then a fresh session is
|
|
441
|
-
* started on the target agent carrying only the task id — never the caller's
|
|
442
|
-
* context. The session start is create-once on the task id, so a retried
|
|
443
|
-
* tool call cannot start a second session.
|
|
444
|
-
*/
|
|
445
|
-
function invokeAgentTool(stores, options) {
|
|
446
|
-
const doFetch = options.fetch ?? fetch;
|
|
447
|
-
return defineTool({
|
|
448
|
-
description: `${options.description} Hands the work to a separate agent in a fresh session; you will not see its reasoning. Returns the handoff task id.`,
|
|
449
|
-
inputSchema: z.object({
|
|
450
|
-
repositoryIds: z.array(repositoryIdSchema).min(1),
|
|
451
|
-
parentTaskId: taskIdSchema.optional().describe("Your own task, when this work continues it."),
|
|
452
|
-
changeId: changeIdSchema.optional().describe("The change the target agent should work on."),
|
|
453
|
-
channel: z.string().min(1).describe("Reply connector, e.g. github."),
|
|
454
|
-
address: z.string().min(1).describe("Reply address, e.g. acme/web#41."),
|
|
455
|
-
brief: z.string().min(1).max(4e3).describe("What the target agent needs to know. Facts and references only, not your reasoning.")
|
|
456
|
-
}),
|
|
457
|
-
async execute(input) {
|
|
458
|
-
const task = await stores.tasks.create({
|
|
459
|
-
repositoryIds: input.repositoryIds,
|
|
460
|
-
kind: options.kind,
|
|
461
|
-
origin: input.parentTaskId === void 0 ? { operator: "factory" } : { parentTaskId: input.parentTaskId },
|
|
462
|
-
replyTo: {
|
|
463
|
-
channel: input.channel,
|
|
464
|
-
address: input.address
|
|
465
|
-
}
|
|
466
|
-
});
|
|
467
|
-
const running = await dispatchTask(stores, task.id, {
|
|
468
|
-
maxLaunchAttempts: 1,
|
|
469
|
-
budget: options.budget?.(),
|
|
470
|
-
launch: async (dispatched) => {
|
|
471
|
-
const headers = {
|
|
472
|
-
"content-type": "application/json",
|
|
473
|
-
...await options.headers?.(dispatched)
|
|
474
|
-
};
|
|
475
|
-
const token = await options.token?.();
|
|
476
|
-
if (token !== void 0) headers.authorization = `Bearer ${token}`;
|
|
477
|
-
const message = [
|
|
478
|
-
`You are assigned factory task ${dispatched.id}.`,
|
|
479
|
-
input.changeId === void 0 ? null : `Change under review: ${input.changeId}.`,
|
|
480
|
-
`Reply address: ${input.channel} ${input.address}.`,
|
|
481
|
-
"",
|
|
482
|
-
input.brief,
|
|
483
|
-
"",
|
|
484
|
-
"Read the task with get_task first, do the work, and record the outcome with your factory tools."
|
|
485
|
-
].filter((line) => line !== null).join("\n");
|
|
486
|
-
const response = await doFetch(`${options.targetUrl()}/eve/v1/session`, {
|
|
487
|
-
method: "POST",
|
|
488
|
-
headers,
|
|
489
|
-
body: JSON.stringify({
|
|
490
|
-
message,
|
|
491
|
-
operationId: `factory-task:${dispatched.id}`
|
|
492
|
-
})
|
|
493
|
-
});
|
|
494
|
-
if (!response.ok) throw new Error(`target agent refused the session: ${response.status}`);
|
|
495
|
-
}
|
|
496
|
-
});
|
|
497
|
-
return {
|
|
498
|
-
taskId: running.id,
|
|
499
|
-
kind: running.kind,
|
|
500
|
-
state: running.state
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
/**
|
|
506
|
-
* Give a conversational agent one bounded delegation surface backed by the
|
|
507
|
-
* application's route registry. The caller selects a declared route; trusted
|
|
508
|
-
* code owns task creation, budget enforcement, and the fresh target session.
|
|
509
|
-
*/
|
|
510
|
-
function delegateAgentTool(stores, options) {
|
|
511
|
-
const doFetch = options.fetch ?? fetch;
|
|
512
|
-
const routeSummary = options.routes.routes.map((route) => `${route.id}: ${route.description}`).join("\n");
|
|
513
|
-
return defineTool({
|
|
514
|
-
description: "Delegate durable work to one registered specialist. Select only a listed route; the specialist runs independently and reports its result through Factory.\n" + routeSummary,
|
|
515
|
-
inputSchema: z.object({
|
|
516
|
-
routeId: z.string().min(1).describe("One route id listed in this tool's description."),
|
|
517
|
-
repositoryIds: z.array(repositoryIdSchema).min(1),
|
|
518
|
-
signalId: signalIdSchema.optional().describe("Pending external signal this delegation accepts."),
|
|
519
|
-
parentTaskId: taskIdSchema.optional().describe("Existing task this work continues."),
|
|
520
|
-
changeId: changeIdSchema.optional().describe("Existing change the specialist should inspect."),
|
|
521
|
-
channel: z.string().min(1).describe("Originating conversation channel."),
|
|
522
|
-
address: z.string().min(1).describe("Originating conversation reply address."),
|
|
523
|
-
brief: z.string().min(1).max(4e3).describe("Facts and desired outcome, without hidden reasoning.")
|
|
524
|
-
}),
|
|
525
|
-
async execute(input, ctx) {
|
|
526
|
-
const route = options.routes.get(input.routeId);
|
|
527
|
-
if (route === void 0) throw new Error(`Unknown agent route ${input.routeId}.`);
|
|
528
|
-
const replyTo = {
|
|
529
|
-
channel: input.channel,
|
|
530
|
-
address: input.address
|
|
531
|
-
};
|
|
532
|
-
const attributes = ctx.session.auth.initiator?.attributes;
|
|
533
|
-
const repositoryAttribute = attributes?.factoryRepositoryIds;
|
|
534
|
-
const boundRepositoryIds = typeof repositoryAttribute === "string" ? repositoryAttribute.split(",").filter(Boolean) : repositoryAttribute;
|
|
535
|
-
if (boundRepositoryIds !== void 0 && (boundRepositoryIds.length !== input.repositoryIds.length || boundRepositoryIds.some((id) => !input.repositoryIds.includes(id)))) throw new Error("Delegation repositories do not match the connector-authenticated context.");
|
|
536
|
-
if (attributes?.factoryReplyChannel !== void 0 && attributes.factoryReplyChannel !== replyTo.channel || attributes?.factoryReplyAddress !== void 0 && attributes.factoryReplyAddress !== replyTo.address) throw new Error("Delegation reply address does not match the authenticated conversation.");
|
|
537
|
-
for (const repositoryId of input.repositoryIds) {
|
|
538
|
-
const repository = await stores.repositories.get(repositoryId);
|
|
539
|
-
if (repository === null || !repository.enabled) throw new Error(`Repository ${repositoryId} is not enabled in this factory.`);
|
|
540
|
-
}
|
|
541
|
-
const boundChange = changeIdSchema.safeParse(attributes?.factoryChangeId);
|
|
542
|
-
if (attributes?.factoryChangeId !== void 0 && !boundChange.success) throw new Error("Connector-authenticated change context is invalid.");
|
|
543
|
-
if (input.changeId !== void 0 && boundChange.success && input.changeId !== boundChange.data) throw new Error("Delegated change does not match the connector-authenticated context.");
|
|
544
|
-
const changeId = input.changeId ?? (boundChange.success ? boundChange.data : void 0);
|
|
545
|
-
if (changeId !== void 0) {
|
|
546
|
-
if (input.repositoryIds.length !== 1) throw new Error("Delegating work on an existing Change requires exactly one repository.");
|
|
547
|
-
const change = await stores.changes.get(changeId);
|
|
548
|
-
if (change === null || change.repositoryId !== input.repositoryIds[0] || change.state !== "verifying" && change.state !== "ready") throw new Error(`Change ${changeId} is not an open pull request in the selected repository.`);
|
|
549
|
-
}
|
|
550
|
-
if (input.signalId !== void 0) {
|
|
551
|
-
const signal = await stores.signals.get(input.signalId);
|
|
552
|
-
if (signal === null) throw new Error(`Signal ${input.signalId} does not exist.`);
|
|
553
|
-
if (!input.repositoryIds.includes(signal.repositoryId) || signal.replyTo.channel !== replyTo.channel || signal.replyTo.address !== replyTo.address) throw new Error("Delegation does not match the signal's authenticated repository and reply address.");
|
|
554
|
-
if (signal.outcome === "pending") await stores.signals.decide(signal.id, "accepted", { reason: `Foreman selected ${route.id}.` });
|
|
555
|
-
else if (signal.outcome !== "accepted") throw new Error(`Signal ${signal.id} is already ${signal.outcome}.`);
|
|
556
|
-
}
|
|
557
|
-
const operator = ctx.session.auth.initiator?.principalId ?? "foreman";
|
|
558
|
-
if (route.taskKind === "code_change" && input.repositoryIds.length > 1) {
|
|
559
|
-
const [first, second, ...rest] = input.repositoryIds;
|
|
560
|
-
if (first === void 0 || second === void 0) throw new Error("Coordination requires two repositories.");
|
|
561
|
-
const coordination = await createCoordination(stores, {
|
|
562
|
-
repositoryIds: [
|
|
563
|
-
first,
|
|
564
|
-
second,
|
|
565
|
-
...rest
|
|
566
|
-
],
|
|
567
|
-
replyTo,
|
|
568
|
-
...input.signalId === void 0 ? {} : { dedupeKey: `route:${input.signalId}:${route.id}` },
|
|
569
|
-
...input.signalId === void 0 ? { operator } : { signalId: input.signalId }
|
|
570
|
-
});
|
|
571
|
-
await Promise.all(coordination.children.filter((child) => child.state === "queued").map((child) => dispatchRouteTask(stores, options, doFetch, route, child, changeId, input.brief)));
|
|
572
|
-
return {
|
|
573
|
-
taskId: coordination.parent.id,
|
|
574
|
-
childTaskIds: coordination.children.map(({ id }) => id),
|
|
575
|
-
routeId: route.id,
|
|
576
|
-
agent: route.agent,
|
|
577
|
-
state: coordination.parent.state
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
const task = await stores.tasks.create({
|
|
581
|
-
repositoryIds: input.repositoryIds,
|
|
582
|
-
kind: route.taskKind,
|
|
583
|
-
origin: input.signalId !== void 0 ? { signalId: input.signalId } : input.parentTaskId !== void 0 ? { parentTaskId: input.parentTaskId } : { operator },
|
|
584
|
-
replyTo,
|
|
585
|
-
changeId,
|
|
586
|
-
...input.signalId === void 0 ? {} : { dedupeKey: `route:${input.signalId}:${route.id}` }
|
|
587
|
-
});
|
|
588
|
-
const running = task.state === "queued" ? await dispatchRouteTask(stores, options, doFetch, route, task, changeId, input.brief) : task;
|
|
589
|
-
return {
|
|
590
|
-
taskId: running.id,
|
|
591
|
-
routeId: route.id,
|
|
592
|
-
agent: route.agent,
|
|
593
|
-
state: running.state
|
|
594
|
-
};
|
|
595
|
-
}
|
|
596
|
-
});
|
|
597
|
-
}
|
|
598
|
-
async function dispatchRouteTask(stores, options, doFetch, route, task, changeId, brief) {
|
|
599
|
-
return dispatchTask(stores, task.id, {
|
|
600
|
-
maxLaunchAttempts: 1,
|
|
601
|
-
budget: options.budget?.(route),
|
|
602
|
-
launch: async (dispatched) => {
|
|
603
|
-
const headers = {
|
|
604
|
-
"content-type": "application/json",
|
|
605
|
-
...await options.headers?.(dispatched, route)
|
|
606
|
-
};
|
|
607
|
-
const token = await options.token?.(route);
|
|
608
|
-
if (token !== void 0) headers.authorization = `Bearer ${token}`;
|
|
609
|
-
const message = [
|
|
610
|
-
`You are assigned factory task ${dispatched.id} through route ${route.id}.`,
|
|
611
|
-
changeId === void 0 ? null : `Change under review: ${changeId}.`,
|
|
612
|
-
brief,
|
|
613
|
-
"Read the task first and return exactly one bounded Factory outcome."
|
|
614
|
-
].filter((line) => line !== null).join("\n\n");
|
|
615
|
-
const response = await doFetch(`${options.targetUrl(route)}/eve/v1/session`, {
|
|
616
|
-
method: "POST",
|
|
617
|
-
headers,
|
|
618
|
-
body: JSON.stringify({
|
|
619
|
-
message,
|
|
620
|
-
operationId: `factory-task:${dispatched.id}`
|
|
621
|
-
})
|
|
622
|
-
});
|
|
623
|
-
if (!response.ok) throw new Error(`${route.agent} refused the session: ${response.status}`);
|
|
624
|
-
}
|
|
625
|
-
});
|
|
626
|
-
}
|
|
627
|
-
//#endregion
|
|
628
|
-
//#region src/eve/post-review.ts
|
|
629
|
-
const reviewCommentSchema = z.strictObject({
|
|
630
|
-
key: z.string().min(1).max(80).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).optional().describe("Stable kebab-case identity for this defect and cause; keep it unchanged across reruns."),
|
|
631
|
-
title: z.string().min(1).max(200).optional().describe("Short finding title."),
|
|
632
|
-
path: z.string().min(1).max(500).describe("Repository-relative file path."),
|
|
633
|
-
line: z.number().int().min(1).describe("Line in the pull request's new version."),
|
|
634
|
-
body: z.string().min(1).max(5e3).describe("Markdown explaining the finding and a concrete fix.")
|
|
635
|
-
});
|
|
636
|
-
const reviewCommentsSchema = z.array(reviewCommentSchema).max(50).default([]).superRefine((comments, context) => {
|
|
637
|
-
const seen = /* @__PURE__ */ new Set();
|
|
638
|
-
comments.forEach((comment, index) => {
|
|
639
|
-
if (comment.key === void 0) return;
|
|
640
|
-
if (seen.has(comment.key)) context.addIssue({
|
|
641
|
-
code: "custom",
|
|
642
|
-
message: `duplicate finding key: ${comment.key}`,
|
|
643
|
-
path: [index, "key"]
|
|
644
|
-
});
|
|
645
|
-
seen.add(comment.key);
|
|
646
|
-
});
|
|
647
|
-
});
|
|
648
|
-
/**
|
|
649
|
-
* Publish an independent review of an existing pull request and finish the
|
|
650
|
-
* review task without changing the pull request's factory lifecycle.
|
|
651
|
-
*/
|
|
652
|
-
function postReviewTool(stores, options) {
|
|
653
|
-
return defineTool({
|
|
654
|
-
description: "Record a completed code review and finish your code_review task. Results may be reported through completion callbacks instead of a GitHub review; use request_changes only for blocking findings.",
|
|
655
|
-
inputSchema: z.object({
|
|
656
|
-
taskId: taskIdSchema.describe("Your code_review task."),
|
|
657
|
-
changeId: changeIdSchema.describe("The pull request change from your starting message."),
|
|
658
|
-
verdict: z.enum([
|
|
659
|
-
"approve",
|
|
660
|
-
"request_changes",
|
|
661
|
-
"comment"
|
|
662
|
-
]),
|
|
663
|
-
headSha: z.string().regex(/^[0-9a-f]{40}$/).describe("Commit returned by fetch_pull_request."),
|
|
664
|
-
summary: z.string().min(1).max(5e3).describe("Markdown summary of what you reviewed and found."),
|
|
665
|
-
comments: reviewCommentsSchema
|
|
666
|
-
}),
|
|
667
|
-
async execute({ taskId, changeId, verdict, headSha, summary, comments }) {
|
|
668
|
-
const task = await stores.tasks.get(taskId);
|
|
669
|
-
if (task === null || task.kind !== "code_review") return {
|
|
670
|
-
recorded: false,
|
|
671
|
-
reason: `${taskId} is not a code_review task`
|
|
672
|
-
};
|
|
673
|
-
if (task.state !== "running") return {
|
|
674
|
-
recorded: false,
|
|
675
|
-
reason: `task is ${task.state}, not running`
|
|
676
|
-
};
|
|
677
|
-
const change = await stores.changes.get(changeId);
|
|
678
|
-
if (change === null) return {
|
|
679
|
-
recorded: false,
|
|
680
|
-
reason: `change ${changeId} does not exist`
|
|
681
|
-
};
|
|
682
|
-
if (change.state !== "verifying" && change.state !== "ready") return {
|
|
683
|
-
recorded: false,
|
|
684
|
-
reason: `change is ${change.state}, not open for review`
|
|
685
|
-
};
|
|
686
|
-
const repository = await stores.repositories.get(change.repositoryId);
|
|
687
|
-
if (repository === null) return {
|
|
688
|
-
recorded: false,
|
|
689
|
-
reason: `repository ${change.repositoryId} does not exist`
|
|
690
|
-
};
|
|
691
|
-
const token = await options.tokenForRepository(repository.slug);
|
|
692
|
-
const completedComments = comments.map((comment) => {
|
|
693
|
-
const key = comment.key ?? legacyFindingKey(comment);
|
|
694
|
-
return {
|
|
695
|
-
...comment,
|
|
696
|
-
key,
|
|
697
|
-
title: comment.title ?? "Review finding",
|
|
698
|
-
fingerprint: createHash("sha256").update(`${repository.slug.toLowerCase()}#${change.prNumber}:${key}`).digest("hex")
|
|
699
|
-
};
|
|
700
|
-
});
|
|
701
|
-
const completed = {
|
|
702
|
-
taskId,
|
|
703
|
-
changeId,
|
|
704
|
-
repository: repository.slug,
|
|
705
|
-
pullRequestNumber: change.prNumber,
|
|
706
|
-
headSha,
|
|
707
|
-
verdict,
|
|
708
|
-
summary,
|
|
709
|
-
comments: completedComments
|
|
710
|
-
};
|
|
711
|
-
const publish = options.publishReviews !== false;
|
|
712
|
-
let review;
|
|
713
|
-
if (publish) {
|
|
714
|
-
review = await submitPullRequestReview({
|
|
715
|
-
repository: repository.slug,
|
|
716
|
-
token,
|
|
717
|
-
fetch: options.fetch,
|
|
718
|
-
pullRequestNumber: change.prNumber,
|
|
719
|
-
commitId: headSha,
|
|
720
|
-
event: verdict === "approve" ? "APPROVE" : verdict === "request_changes" ? "REQUEST_CHANGES" : "COMMENT",
|
|
721
|
-
body: summary,
|
|
722
|
-
comments: completedComments
|
|
723
|
-
});
|
|
724
|
-
await options.onPublished?.({
|
|
725
|
-
...completed,
|
|
726
|
-
review
|
|
727
|
-
});
|
|
728
|
-
} else {
|
|
729
|
-
const head = await readPullRequestHead({
|
|
730
|
-
repository: repository.slug,
|
|
731
|
-
token,
|
|
732
|
-
fetch: options.fetch,
|
|
733
|
-
pullRequestNumber: change.prNumber
|
|
734
|
-
});
|
|
735
|
-
if (head.sha !== headSha) throw new Error(`pull request changed after review: expected ${headSha}, found ${head.sha}`);
|
|
736
|
-
}
|
|
737
|
-
await options.onCompleted?.(completed);
|
|
738
|
-
await stores.tasks.transition(taskId, "verifying", { reason: summary });
|
|
739
|
-
const finished = await stores.tasks.transition(taskId, "succeeded");
|
|
740
|
-
return {
|
|
741
|
-
recorded: true,
|
|
742
|
-
verdict,
|
|
743
|
-
published: review !== void 0,
|
|
744
|
-
...review === void 0 ? {} : { review },
|
|
745
|
-
taskState: finished.state
|
|
746
|
-
};
|
|
747
|
-
}
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
function legacyFindingKey(comment) {
|
|
751
|
-
return `legacy-${createHash("sha256").update(`${comment.path}:${comment.line}:${comment.body}`).digest("hex").slice(0, 24)}`;
|
|
752
|
-
}
|
|
753
|
-
//#endregion
|
|
754
|
-
//#region src/eve/review.ts
|
|
755
|
-
function reviewCommentTools(stores, options) {
|
|
756
|
-
const repoPath = options.repoPath ?? "repo";
|
|
757
|
-
const commentInputSchema = z.object({
|
|
758
|
-
changeId: changeIdSchema.describe("The change under review, from your starting message."),
|
|
759
|
-
commentId: z.number().int().positive().describe("The GitHub review comment id.")
|
|
760
|
-
});
|
|
761
|
-
return {
|
|
762
|
-
read_review_comment: defineTool({
|
|
763
|
-
description: "Read an inline review comment on a change under verification or ready for human review. By default this refuses to return comments from people without write access to the repository.",
|
|
764
|
-
inputSchema: commentInputSchema,
|
|
765
|
-
async execute({ changeId, commentId }) {
|
|
766
|
-
const change = await stores.changes.get(changeId);
|
|
767
|
-
if (change === null) throw new Error(`change ${changeId} does not exist`);
|
|
768
|
-
if (change.state !== "verifying" && change.state !== "ready") throw new Error(`change is ${change.state}, not open for review`);
|
|
769
|
-
const repository = await stores.repositories.get(change.repositoryId);
|
|
770
|
-
if (repository === null) throw new Error(`repository ${change.repositoryId} does not exist`);
|
|
771
|
-
return readReviewComment({
|
|
772
|
-
repository: repository.slug,
|
|
773
|
-
token: await options.tokenForRepository(repository.slug),
|
|
774
|
-
fetch: options.fetch,
|
|
775
|
-
commentId
|
|
776
|
-
});
|
|
777
|
-
}
|
|
778
|
-
}),
|
|
779
|
-
answer_review_comment: defineTool({
|
|
780
|
-
description: "Reply to an inline review comment on a change under verification or ready for human review. Use for findings and low-confidence responses, not for a concrete medium- or high-confidence fix.",
|
|
781
|
-
inputSchema: commentInputSchema.extend({ body: z.string().min(1).max(5e3).describe("The Markdown reply to post.") }),
|
|
782
|
-
async execute({ changeId, commentId, body }) {
|
|
783
|
-
const change = await stores.changes.get(changeId);
|
|
784
|
-
if (change === null) throw new Error(`change ${changeId} does not exist`);
|
|
785
|
-
if (change.state !== "verifying" && change.state !== "ready") throw new Error(`change is ${change.state}, not open for review`);
|
|
786
|
-
const repository = await stores.repositories.get(change.repositoryId);
|
|
787
|
-
if (repository === null) throw new Error(`repository ${change.repositoryId} does not exist`);
|
|
788
|
-
return answerReviewComment({
|
|
789
|
-
repository: repository.slug,
|
|
790
|
-
token: await options.tokenForRepository(repository.slug),
|
|
791
|
-
fetch: options.fetch,
|
|
792
|
-
pullRequestNumber: change.prNumber,
|
|
793
|
-
commentId,
|
|
794
|
-
body
|
|
795
|
-
});
|
|
796
|
-
}
|
|
797
|
-
}),
|
|
798
|
-
suggest_review_fix: defineTool({
|
|
799
|
-
description: "Post a concrete, apply-able GitHub suggested change for a medium-confidence fix. The replacement must cover exactly the lines attached to the inline review comment.",
|
|
800
|
-
inputSchema: commentInputSchema.extend({
|
|
801
|
-
confidence: z.literal("medium"),
|
|
802
|
-
replacement: z.string().min(1).max(5e3).describe("Exact replacement text for the commented lines."),
|
|
803
|
-
summary: z.string().min(1).max(1e3).describe("A brief explanation of the suggested fix.")
|
|
804
|
-
}),
|
|
805
|
-
async execute({ changeId, commentId, replacement, summary }) {
|
|
806
|
-
const change = await stores.changes.get(changeId);
|
|
807
|
-
if (change === null) throw new Error(`change ${changeId} does not exist`);
|
|
808
|
-
if (change.state !== "verifying" && change.state !== "ready") throw new Error(`change is ${change.state}, not open for review`);
|
|
809
|
-
const repository = await stores.repositories.get(change.repositoryId);
|
|
810
|
-
if (repository === null) throw new Error(`repository ${change.repositoryId} does not exist`);
|
|
811
|
-
return answerReviewComment({
|
|
812
|
-
repository: repository.slug,
|
|
813
|
-
token: await options.tokenForRepository(repository.slug),
|
|
814
|
-
fetch: options.fetch,
|
|
815
|
-
pullRequestNumber: change.prNumber,
|
|
816
|
-
commentId,
|
|
817
|
-
body: `${summary}\n\n\`\`\`suggestion\n${replacement}\n\`\`\``
|
|
818
|
-
});
|
|
819
|
-
}
|
|
820
|
-
}),
|
|
821
|
-
apply_review_fix: defineTool({
|
|
822
|
-
description: "Publish a tested, committed high-confidence fix to the existing same-repository pull request branch. Fails if the checkout is dirty, the PR changed, or the PR comes from a fork.",
|
|
823
|
-
inputSchema: commentInputSchema.extend({
|
|
824
|
-
confidence: z.literal("high"),
|
|
825
|
-
summary: z.string().min(1).max(1e3).describe("What was fixed and verified.")
|
|
826
|
-
}),
|
|
827
|
-
async execute({ changeId, commentId, summary }, ctx) {
|
|
828
|
-
const change = await stores.changes.get(changeId);
|
|
829
|
-
if (change === null) throw new Error(`change ${changeId} does not exist`);
|
|
830
|
-
if (change.state !== "verifying" && change.state !== "ready") throw new Error(`change is ${change.state}, not open for review`);
|
|
831
|
-
const repository = await stores.repositories.get(change.repositoryId);
|
|
832
|
-
if (repository === null) throw new Error(`repository ${change.repositoryId} does not exist`);
|
|
833
|
-
if (options.withCredential === void 0) throw new Error("applying review fixes is not configured");
|
|
834
|
-
const token = await options.tokenForRepository(repository.slug);
|
|
835
|
-
await readReviewComment({
|
|
836
|
-
repository: repository.slug,
|
|
837
|
-
token,
|
|
838
|
-
fetch: options.fetch,
|
|
839
|
-
commentId
|
|
840
|
-
});
|
|
841
|
-
const head = await readPullRequestHead({
|
|
842
|
-
repository: repository.slug,
|
|
843
|
-
token,
|
|
844
|
-
fetch: options.fetch,
|
|
845
|
-
pullRequestNumber: change.prNumber
|
|
846
|
-
});
|
|
847
|
-
if (head.repository !== repository.slug.toLowerCase()) throw new Error("cannot apply a fix to a pull request from a fork");
|
|
848
|
-
const sandbox = await ctx.getSandbox();
|
|
849
|
-
const git = `git -C ${repoPath}`;
|
|
850
|
-
if ((await sandbox.run({ command: `${git} status --porcelain` })).stdout.trim() !== "") throw new Error("commit the review fix before publishing");
|
|
851
|
-
if ((await sandbox.run({ command: `${git} merge-base --is-ancestor ${head.sha} HEAD` })).exitCode !== 0) throw new Error("pull request changed after it was fetched; fetch it again before applying the fix");
|
|
852
|
-
if ((await sandbox.run({ command: `${git} rev-parse HEAD` })).stdout.trim() === head.sha) throw new Error("no committed review fix to publish");
|
|
853
|
-
const push = await options.withCredential(sandbox, repository.slug, `${git} push --force-with-lease=refs/heads/${head.branch}:${head.sha} origin HEAD:refs/heads/${head.branch}`);
|
|
854
|
-
if (typeof push.exitCode === "number" && push.exitCode !== 0) throw new Error(`could not update pull request branch: ${(push.stderr ?? "").trim()}`);
|
|
855
|
-
const reply = await answerReviewComment({
|
|
856
|
-
repository: repository.slug,
|
|
857
|
-
token,
|
|
858
|
-
fetch: options.fetch,
|
|
859
|
-
pullRequestNumber: change.prNumber,
|
|
860
|
-
commentId,
|
|
861
|
-
body: summary
|
|
862
|
-
});
|
|
863
|
-
return {
|
|
864
|
-
updated: true,
|
|
865
|
-
prNumber: change.prNumber,
|
|
866
|
-
prUrl: change.prUrl,
|
|
867
|
-
reply
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
|
-
})
|
|
871
|
-
};
|
|
872
|
-
}
|
|
873
|
-
//#endregion
|
|
874
|
-
//#region src/eve/verification.ts
|
|
875
|
-
/**
|
|
876
|
-
* The verifier's outcome tool. Writes evidence onto the change and moves it
|
|
877
|
-
* to ready only when the verdict is pass; a failed verdict leaves the change
|
|
878
|
-
* verifying with the evidence recorded, and the verification task itself
|
|
879
|
-
* ends with a receipt either way. The change's own code_change task follows
|
|
880
|
-
* the verdict too: succeeded on pass, failed on fail with the verifier's
|
|
881
|
-
* summary as the reason, so the ledger never shows a verified-and-rejected fix
|
|
882
|
-
* as still in progress. Fail closed: the agent cannot mark a change ready
|
|
883
|
-
* without supplying every evidence field.
|
|
884
|
-
*/
|
|
885
|
-
function recordVerificationTool(stores, options = {}) {
|
|
886
|
-
return defineTool({
|
|
887
|
-
description: "Record the outcome of a verification task: the evidence you gathered and whether the change passed. Pass moves the change to ready for human review; fail keeps it out of review.",
|
|
888
|
-
inputSchema: z.object({
|
|
889
|
-
taskId: taskIdSchema.describe("Your verification task."),
|
|
890
|
-
changeId: changeIdSchema.describe("The change you verified."),
|
|
891
|
-
verdict: z.enum(["pass", "fail"]),
|
|
892
|
-
evidence: changeEvidenceSchema,
|
|
893
|
-
confidence: levelSchema.describe("How certain you are in the verdict."),
|
|
894
|
-
summary: z.string().min(1).max(2e3).describe("What you checked and what you found.")
|
|
895
|
-
}),
|
|
896
|
-
async execute({ taskId, changeId, verdict, evidence, confidence, summary }) {
|
|
897
|
-
const change = await stores.changes.get(changeId);
|
|
898
|
-
if (change === null) return {
|
|
899
|
-
recorded: false,
|
|
900
|
-
reason: `change ${changeId} does not exist`
|
|
901
|
-
};
|
|
902
|
-
if (change.state !== "verifying" && change.state !== "ready") return {
|
|
903
|
-
recorded: false,
|
|
904
|
-
reason: `change is ${change.state}, not open for verification; nothing to record`
|
|
905
|
-
};
|
|
906
|
-
const task = await stores.tasks.get(taskId);
|
|
907
|
-
if (task === null || task.kind !== "verification") return {
|
|
908
|
-
recorded: false,
|
|
909
|
-
reason: `${taskId} is not a verification task`
|
|
910
|
-
};
|
|
911
|
-
let updatedChange = change;
|
|
912
|
-
if (verdict === "pass" && change.state === "verifying") updatedChange = await stores.changes.transition(changeId, "ready", {
|
|
913
|
-
patch: {
|
|
914
|
-
evidence,
|
|
915
|
-
confidence
|
|
916
|
-
},
|
|
917
|
-
reason: summary
|
|
918
|
-
});
|
|
919
|
-
else if (verdict === "fail" && change.state === "ready") updatedChange = await stores.changes.transition(changeId, "verifying", { reason: summary });
|
|
920
|
-
let finished;
|
|
921
|
-
if (verdict === "pass") {
|
|
922
|
-
await stores.tasks.transition(taskId, "verifying", { reason: summary });
|
|
923
|
-
finished = await stores.tasks.transition(taskId, "succeeded");
|
|
924
|
-
} else finished = await stores.tasks.transition(taskId, "failed", { reason: summary });
|
|
925
|
-
const sourceTaskId = task.origin.parentTaskId ?? change.taskId;
|
|
926
|
-
if (sourceTaskId !== void 0) {
|
|
927
|
-
if ((await stores.tasks.get(sourceTaskId))?.state === "verifying") await stores.tasks.transition(sourceTaskId, verdict === "pass" ? "succeeded" : "failed", verdict === "pass" ? {} : { reason: `verification failed: ${summary}` }).catch(() => void 0);
|
|
928
|
-
}
|
|
929
|
-
await options.presentResult?.({
|
|
930
|
-
taskId,
|
|
931
|
-
changeId,
|
|
932
|
-
verdict,
|
|
933
|
-
summary
|
|
934
|
-
});
|
|
935
|
-
return {
|
|
936
|
-
recorded: true,
|
|
937
|
-
verdict,
|
|
938
|
-
changeState: updatedChange.state,
|
|
939
|
-
taskState: finished.state
|
|
940
|
-
};
|
|
941
|
-
}
|
|
942
|
-
});
|
|
943
|
-
}
|
|
944
|
-
//#endregion
|
|
945
13
|
//#region src/eve/index.ts
|
|
946
14
|
const decidedOutcomeSchema = z.enum([
|
|
947
15
|
"accepted",
|
|
@@ -950,46 +18,89 @@ const decidedOutcomeSchema = z.enum([
|
|
|
950
18
|
"low_confidence",
|
|
951
19
|
"ignored"
|
|
952
20
|
]);
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
prNumber: z.number().int().min(1),
|
|
957
|
-
prUrl: z.url(),
|
|
958
|
-
risk: levelSchema.describe("Blast radius of the change if it is wrong."),
|
|
959
|
-
confidence: levelSchema.describe("How certain you are the change is correct."),
|
|
960
|
-
summary: z.string().min(1).describe("One-sentence description of the change.")
|
|
961
|
-
});
|
|
962
|
-
async function createAndPresentQuestion(stores, parent, question, presentQuestion) {
|
|
21
|
+
async function createAndPresentQuestion(stores, parent, question, presentQuestion, completedSessionId) {
|
|
22
|
+
const needsHumanAction = question.text.trim();
|
|
23
|
+
if (needsHumanAction === "") throw new Error("A human question must contain non-blank text.");
|
|
963
24
|
const task = await stores.tasks.create({
|
|
964
25
|
repositoryIds: parent.repositoryIds,
|
|
965
26
|
kind: "question",
|
|
966
|
-
|
|
27
|
+
work: taskWork({
|
|
28
|
+
title: needsHumanAction.slice(0, 256),
|
|
29
|
+
input: {
|
|
30
|
+
question,
|
|
31
|
+
...completedSessionId ? {
|
|
32
|
+
systemReason: "unresolved_turn",
|
|
33
|
+
completedSessionId
|
|
34
|
+
} : {}
|
|
35
|
+
}
|
|
36
|
+
}),
|
|
37
|
+
parentTaskId: parent.id,
|
|
38
|
+
parentTaskAttempt: parent.attempt,
|
|
39
|
+
origin: {},
|
|
967
40
|
replyTo: parent.replyTo,
|
|
968
41
|
question
|
|
969
42
|
});
|
|
970
|
-
const waiting = await stores.tasks.transition(task.id, "needs_human");
|
|
971
43
|
try {
|
|
44
|
+
const waiting = await stores.tasks.transition(task.id, "needs_human", { needsHumanAction });
|
|
972
45
|
if (parent.state === "running") await stores.tasks.transition(parent.id, "needs_human", {
|
|
46
|
+
expectFrom: "running",
|
|
47
|
+
expectAttempt: parent.attempt,
|
|
973
48
|
reason: `waiting for answer to question ${waiting.id}`,
|
|
974
|
-
questionTaskId: waiting.id
|
|
49
|
+
questionTaskId: waiting.id,
|
|
50
|
+
needsHumanAction
|
|
975
51
|
});
|
|
976
52
|
await presentQuestion(waiting);
|
|
53
|
+
return waiting;
|
|
977
54
|
} catch (error) {
|
|
978
|
-
|
|
979
|
-
if (
|
|
55
|
+
const currentParent = await stores.tasks.get(parent.id);
|
|
56
|
+
if (currentParent?.state === "needs_human" && currentParent.attempt === parent.attempt && currentParent.questionTaskId === task.id) try {
|
|
57
|
+
await stores.tasks.transition(parent.id, "queued", {
|
|
58
|
+
expectFrom: "needs_human",
|
|
59
|
+
expectAttempt: currentParent.attempt,
|
|
60
|
+
expectQuestionTaskId: task.id,
|
|
61
|
+
reason: `delivery of question ${task.id} failed`
|
|
62
|
+
});
|
|
63
|
+
} catch (cleanupError) {
|
|
64
|
+
const latest = await stores.tasks.get(parent.id);
|
|
65
|
+
if (latest?.state === "needs_human" && latest.attempt === currentParent.attempt && latest.questionTaskId === task.id) throw cleanupError;
|
|
66
|
+
}
|
|
67
|
+
const currentQuestion = await stores.tasks.get(task.id);
|
|
68
|
+
if (currentQuestion?.state === "needs_human" || currentQuestion?.state === "queued") await stores.tasks.transition(task.id, "cancelled", {
|
|
69
|
+
expectFrom: currentQuestion.state,
|
|
70
|
+
expectAttempt: currentQuestion.attempt,
|
|
71
|
+
reason: "question delivery failed"
|
|
72
|
+
});
|
|
980
73
|
throw error;
|
|
981
74
|
}
|
|
982
|
-
return waiting;
|
|
983
75
|
}
|
|
984
76
|
/**
|
|
985
|
-
*
|
|
986
|
-
*
|
|
987
|
-
*
|
|
988
|
-
* the
|
|
77
|
+
* Wraps provider-neutral Factory store operations as Eve tools.
|
|
78
|
+
*
|
|
79
|
+
* @remarks
|
|
80
|
+
* Expose only the returned tools each agent role needs. Every write goes through the validating
|
|
81
|
+
* engine, but tool visibility remains application authorization policy. `finish_task` and
|
|
82
|
+
* `request_human_input` recheck the current authenticated Task attempt and Eve session before
|
|
83
|
+
* mutating state. `get_task`, signal decisions, and generic receipts are broader capabilities and
|
|
84
|
+
* should be mounted only for agents allowed to use them.
|
|
85
|
+
*
|
|
86
|
+
* The generic `finish_task` records `{ summary }` for `task@1`. Schema-specific workflows should
|
|
87
|
+
* define a narrow tool that calls `requireTaskExecution` and `stores.work.completeWorkflow`, as in
|
|
88
|
+
* the shipped `docs/recipes/typed-eve-result.md` recipe.
|
|
89
|
+
*
|
|
90
|
+
* @param stores - Receipt, Signal, and Task capabilities available to the tool bundle.
|
|
91
|
+
* @param options - Optional question presentation and generic-completion policy.
|
|
92
|
+
* @returns A new named Eve tool bundle ready for selective mounting under `agent/tools/`.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* import { createTaskTools, type TaskToolsStores } from "@vercel/factory";
|
|
97
|
+
*
|
|
98
|
+
* declare const stores: TaskToolsStores;
|
|
99
|
+
* const tools = createTaskTools(stores);
|
|
100
|
+
* export default tools.get_task;
|
|
101
|
+
* ```
|
|
989
102
|
*/
|
|
990
|
-
function
|
|
991
|
-
const recordChangeGate = options.recordChangeGate;
|
|
992
|
-
const verification = options.verification ?? "required";
|
|
103
|
+
function createTaskTools(stores, options = {}) {
|
|
993
104
|
return {
|
|
994
105
|
get_task: defineTool({
|
|
995
106
|
description: "Read one factory task by id: its kind, state, repositories, origin, and results.",
|
|
@@ -1010,105 +121,45 @@ function factoryTools(stores, options = {}) {
|
|
|
1010
121
|
detail: z.string().min(1).optional().describe("Severity if accepted, or the duplicate-of signal id.")
|
|
1011
122
|
}),
|
|
1012
123
|
execute({ signalId, outcome, reason, detail }) {
|
|
1013
|
-
return stores.signals.decide(signalId,
|
|
124
|
+
return stores.signals.decide(signalId, {
|
|
125
|
+
outcome,
|
|
1014
126
|
reason,
|
|
1015
127
|
detail
|
|
1016
128
|
});
|
|
1017
129
|
}
|
|
1018
130
|
}),
|
|
1019
|
-
record_change: defineTool({
|
|
1020
|
-
description: verification === "required" ? "Record the pull request produced for a code_change task. Creates the change, links it to the task, and moves the task to verifying." : "Record the pull request produced for a code_change task. Creates the change and marks it ready for human review without independent verification.",
|
|
1021
|
-
inputSchema: recordChangeInputSchema,
|
|
1022
|
-
...recordChangeGate === void 0 ? {} : { approval: async ({ toolInput }) => {
|
|
1023
|
-
const parsed = recordChangeInputSchema.safeParse(toolInput);
|
|
1024
|
-
if (!parsed.success) return {
|
|
1025
|
-
type: "denied",
|
|
1026
|
-
reason: "record_change input is malformed; fix the arguments and retry"
|
|
1027
|
-
};
|
|
1028
|
-
const verdict = await recordChangeGate(parsed.data);
|
|
1029
|
-
return verdict.ok ? "not-applicable" : {
|
|
1030
|
-
type: "denied",
|
|
1031
|
-
reason: verdict.reason
|
|
1032
|
-
};
|
|
1033
|
-
} },
|
|
1034
|
-
async execute({ taskId, repositoryId, prNumber, prUrl, risk, confidence, summary }) {
|
|
1035
|
-
const currentTask = await stores.tasks.get(taskId);
|
|
1036
|
-
if (currentTask === null) throw new Error(`task ${taskId} does not exist`);
|
|
1037
|
-
if (currentTask.state !== "running" && currentTask.state !== "verifying" && currentTask.state !== "succeeded") throw new Error(`task ${taskId} is ${currentTask.state}, not running`);
|
|
1038
|
-
const linkedChange = currentTask.changeId === void 0 ? void 0 : await stores.changes.get(currentTask.changeId);
|
|
1039
|
-
if (currentTask.changeId !== void 0 && linkedChange === null) throw new Error(`change ${currentTask.changeId} does not exist`);
|
|
1040
|
-
let change = linkedChange ?? (await stores.changes.list()).find((candidate) => candidate.taskId === taskId);
|
|
1041
|
-
if (change === void 0) {
|
|
1042
|
-
if (currentTask.state !== "running") throw new Error(`task ${taskId} has no recorded change`);
|
|
1043
|
-
change = await stores.changes.create({
|
|
1044
|
-
repositoryId,
|
|
1045
|
-
taskId,
|
|
1046
|
-
prNumber,
|
|
1047
|
-
prUrl,
|
|
1048
|
-
author: "factory",
|
|
1049
|
-
risk,
|
|
1050
|
-
confidence,
|
|
1051
|
-
evidence: {
|
|
1052
|
-
reproVerified: false,
|
|
1053
|
-
testsAdded: 0,
|
|
1054
|
-
checksGreen: false,
|
|
1055
|
-
attackPassed: false,
|
|
1056
|
-
scopeMatches: false
|
|
1057
|
-
}
|
|
1058
|
-
});
|
|
1059
|
-
}
|
|
1060
|
-
if (currentTask.state === "running") await options.presentChange?.({
|
|
1061
|
-
taskId,
|
|
1062
|
-
changeId: change.id,
|
|
1063
|
-
verification,
|
|
1064
|
-
summary
|
|
1065
|
-
});
|
|
1066
|
-
let task = currentTask;
|
|
1067
|
-
if (task.state === "running") task = await stores.tasks.transition(taskId, "verifying", {
|
|
1068
|
-
patch: { changeId: change.id },
|
|
1069
|
-
reason: summary
|
|
1070
|
-
});
|
|
1071
|
-
if (verification === "required") return {
|
|
1072
|
-
change,
|
|
1073
|
-
task
|
|
1074
|
-
};
|
|
1075
|
-
if (change.state === "verifying") change = await stores.changes.transition(change.id, "ready", { reason: "independent verification is not configured" });
|
|
1076
|
-
if (task.state === "verifying") task = await stores.tasks.transition(taskId, "succeeded", { reason: "change recorded for human review without independent verification" });
|
|
1077
|
-
return {
|
|
1078
|
-
change,
|
|
1079
|
-
task
|
|
1080
|
-
};
|
|
1081
|
-
}
|
|
1082
|
-
}),
|
|
1083
131
|
finish_task: defineTool({
|
|
1084
|
-
description: "Mark your task finished with a summary
|
|
132
|
+
description: "Mark your running task finished with a summary. The configured workflow policy may require a more specific outcome tool.",
|
|
1085
133
|
inputSchema: z.object({
|
|
1086
134
|
taskId: taskIdSchema,
|
|
1087
135
|
summary: z.string().min(1).max(4e3).describe("What you did and found, for the ledger.")
|
|
1088
136
|
}),
|
|
1089
|
-
async execute({ taskId, summary }) {
|
|
1090
|
-
const task = await stores
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
reason: `${taskId} does not exist`
|
|
1094
|
-
};
|
|
1095
|
-
if (task.kind === "code_change" || task.kind === "verification") return {
|
|
137
|
+
async execute({ taskId, summary }, ctx) {
|
|
138
|
+
const task = await requireTaskExecution(stores, ctx, taskId);
|
|
139
|
+
const refusal = await options.finishTaskPolicy?.(task);
|
|
140
|
+
if (refusal !== void 0 && refusal !== null) return {
|
|
1096
141
|
finished: false,
|
|
1097
|
-
reason:
|
|
142
|
+
reason: refusal
|
|
1098
143
|
};
|
|
1099
144
|
if (task.state !== "running") return {
|
|
1100
145
|
finished: false,
|
|
1101
146
|
reason: `task is ${task.state}, not running`
|
|
1102
147
|
};
|
|
1103
|
-
await stores.tasks.transition(taskId, "
|
|
148
|
+
await stores.tasks.transition(taskId, "succeeded", {
|
|
149
|
+
expectFrom: "running",
|
|
150
|
+
expectAttempt: task.attempt,
|
|
151
|
+
expectExecution: task.execution,
|
|
152
|
+
reason: summary,
|
|
153
|
+
output: { summary }
|
|
154
|
+
});
|
|
1104
155
|
return {
|
|
1105
156
|
finished: true,
|
|
1106
|
-
taskState:
|
|
157
|
+
taskState: "succeeded"
|
|
1107
158
|
};
|
|
1108
159
|
}
|
|
1109
160
|
}),
|
|
1110
161
|
request_human_input: defineTool({
|
|
1111
|
-
description: "Request human input or action and pause your task until the human replies. The parent task determines the repositories and reply destination.",
|
|
162
|
+
description: "Request one human input or action and pause your currently running task until the human replies. A paused task cannot request another question; wait for the answer and replacement execution. The parent task determines the repositories and reply destination. Name any suggested reply in question.closingOptions when that exact answer means no further attempt can help (for example \"nothing to do, close it\"); Factory resolves the task instead of requeuing an identical retry when the human picks one.",
|
|
1112
163
|
inputSchema: z.object({
|
|
1113
164
|
parentTaskId: taskIdSchema.describe("Your running task, which the answer will unblock."),
|
|
1114
165
|
question: taskQuestionSchema.omit({
|
|
@@ -1116,16 +167,13 @@ function factoryTools(stores, options = {}) {
|
|
|
1116
167
|
answeredBy: true
|
|
1117
168
|
})
|
|
1118
169
|
}),
|
|
1119
|
-
async execute({ parentTaskId, question }) {
|
|
170
|
+
async execute({ parentTaskId, question }, ctx) {
|
|
1120
171
|
if (options.presentQuestion === void 0) throw new Error("request_human_input requires a configured question presenter.");
|
|
1121
|
-
|
|
1122
|
-
if (parent === null) throw new Error(`Parent task ${parentTaskId} does not exist.`);
|
|
1123
|
-
if (parent.state !== "running" && parent.state !== "needs_human") throw new Error(`Parent task ${parentTaskId} is ${parent.state}, not running.`);
|
|
1124
|
-
return createAndPresentQuestion(stores, parent, question, options.presentQuestion);
|
|
172
|
+
return createAndPresentQuestion(stores, await requireTaskExecution(stores, ctx, parentTaskId), question, options.presentQuestion);
|
|
1125
173
|
}
|
|
1126
174
|
}),
|
|
1127
175
|
answer_question: defineTool({
|
|
1128
|
-
description: "Record the authenticated user's answer to a waiting question and requeue the parent specialist task. A blocked parent task id is also accepted and resolves its persisted QuestionTask id.",
|
|
176
|
+
description: "Record the authenticated user's answer to a waiting question and requeue the parent specialist task, unless the answer matches one of the question's closingOptions, which resolves the parent task instead. A blocked parent task id is also accepted and resolves its persisted QuestionTask id.",
|
|
1129
177
|
inputSchema: z.object({
|
|
1130
178
|
questionTaskId: taskIdSchema,
|
|
1131
179
|
answer: z.string().min(1).max(4e3)
|
|
@@ -1135,9 +183,9 @@ function factoryTools(stores, options = {}) {
|
|
|
1135
183
|
const resolvedQuestionTaskId = requestedTask?.kind === "question" ? requestedTask.id : requestedTask?.questionTaskId;
|
|
1136
184
|
const questionTask = resolvedQuestionTaskId === void 0 ? null : await stores.tasks.get(resolvedQuestionTaskId);
|
|
1137
185
|
if (questionTask === null || questionTask.kind !== "question" || questionTask.question === void 0) throw new Error(`Question task ${questionTaskId} does not exist.`);
|
|
1138
|
-
const
|
|
1139
|
-
if (attributes
|
|
1140
|
-
const answeredBy =
|
|
186
|
+
const { initiator, current } = ctx.session.auth;
|
|
187
|
+
if (!initiator || !current || [initiator, current].some(({ attributes }) => attributes.factoryReplyChannel !== questionTask.replyTo.channel || attributes.factoryReplyAddress !== questionTask.replyTo.address)) throw new Error("Question reply does not match the authenticated conversation.");
|
|
188
|
+
const answeredBy = current.principalId;
|
|
1141
189
|
return stores.tasks.answerQuestion({
|
|
1142
190
|
questionTaskId: questionTask.id,
|
|
1143
191
|
answer,
|
|
@@ -1148,11 +196,11 @@ function factoryTools(stores, options = {}) {
|
|
|
1148
196
|
append_receipt: defineTool({
|
|
1149
197
|
description: "Append one immutable audit receipt for an event worth remembering. Receipts can never be edited or deleted.",
|
|
1150
198
|
inputSchema: z.object({
|
|
1151
|
-
repositoryIds:
|
|
199
|
+
repositoryIds: repositoryIdsSchema,
|
|
1152
200
|
taskId: taskIdSchema.optional(),
|
|
1153
201
|
changeId: changeIdSchema.optional(),
|
|
1154
|
-
kind:
|
|
1155
|
-
state:
|
|
202
|
+
kind: receiptKindSchema.describe("What the receipt is about, e.g. investigation."),
|
|
203
|
+
state: receiptStateSchema.describe("What happened, e.g. flag_ramped."),
|
|
1156
204
|
reason: z.string().min(1).max(2e3).optional(),
|
|
1157
205
|
dedupeKey: z.string().min(1).optional().describe("Same key twice records one receipt instead of two.")
|
|
1158
206
|
}),
|
|
@@ -1162,62 +210,253 @@ function factoryTools(stores, options = {}) {
|
|
|
1162
210
|
})
|
|
1163
211
|
};
|
|
1164
212
|
}
|
|
213
|
+
function materializationInput(ctx) {
|
|
214
|
+
return {
|
|
215
|
+
agent: ctx.agent.name,
|
|
216
|
+
execution: {
|
|
217
|
+
provider: "eve",
|
|
218
|
+
sessionId: ctx.session.id
|
|
219
|
+
},
|
|
220
|
+
attributes: ctx.session.auth.initiator?.attributes ?? {}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
/** Observe-only hook for agents that do not use the Factory task-lifecycle hook. */
|
|
224
|
+
function createTranscriptSnapshotHook(materialize) {
|
|
225
|
+
const snapshot = (_event, ctx) => materialize(materializationInput(ctx)).catch(() => void 0);
|
|
226
|
+
return defineHook({ events: {
|
|
227
|
+
"session.waiting": snapshot,
|
|
228
|
+
"session.completed": snapshot,
|
|
229
|
+
"session.failed": snapshot
|
|
230
|
+
} });
|
|
231
|
+
}
|
|
1165
232
|
const terminalTaskStates = /* @__PURE__ */ new Set([
|
|
1166
233
|
"succeeded",
|
|
1167
234
|
"failed",
|
|
1168
235
|
"cancelled"
|
|
1169
236
|
]);
|
|
1170
237
|
/**
|
|
1171
|
-
*
|
|
1172
|
-
*
|
|
1173
|
-
* `
|
|
238
|
+
* The turn contract's escalation question: a specialist turn ended cleanly
|
|
239
|
+
* (eve durably recorded `turn.completed`) but the bound task is still
|
|
240
|
+
* `running`, so no transition was recorded. That is never a "worker died
|
|
241
|
+
* mid-turn" situation -- eve confirms the turn finished -- so retrying the
|
|
242
|
+
* identical brief cannot succeed any differently. Escalating straight to
|
|
243
|
+
* `needs_human` instead of waiting for the stall window (up to 45 minutes,
|
|
244
|
+
* times `maxAttempts`) surfaces the dead end immediately. "Close the task"
|
|
245
|
+
* is a declared closingOptions entry: answering with it resolves the task
|
|
246
|
+
* instead of requeuing an identical retry.
|
|
247
|
+
*/
|
|
248
|
+
const UNRESOLVED_TURN_QUESTION = {
|
|
249
|
+
text: "The specialist ended its turn without recording a result for this task. Reply to retry it, or close it if the work no longer applies (for example, a newer attempt already superseded it).",
|
|
250
|
+
options: ["Retry the task", "Close the task"],
|
|
251
|
+
closingOptions: ["Close the task"]
|
|
252
|
+
};
|
|
253
|
+
/**
|
|
254
|
+
* Creates observe-only Eve hooks that project session outcomes onto durable Factory state.
|
|
1174
255
|
*
|
|
256
|
+
* @remarks
|
|
1175
257
|
* step.completed: records the step's tokens and AI Gateway cost on the bound
|
|
1176
|
-
* task. Delivery is at least once; recordUsage dedupes by (turnId, stepIndex).
|
|
258
|
+
* task. Delivery is at least once; recordUsage dedupes by (sessionId, turnId, stepIndex).
|
|
259
|
+
*
|
|
260
|
+
* turn.failed/session.failed: first recovers an opted-in saved workflow result, then re-queues the current attempt, or
|
|
261
|
+
* fails it when its configured attempts are exhausted. turn.cancelled marks
|
|
262
|
+
* the current Task cancelled. Late events from prior attempts are ignored.
|
|
1177
263
|
*
|
|
1178
|
-
* turn.completed:
|
|
1179
|
-
*
|
|
1180
|
-
*
|
|
1181
|
-
*
|
|
264
|
+
* turn.completed: recovers an opted-in saved workflow result before generic escalation, and
|
|
265
|
+
* settles the bound task's recorded usage if the task is
|
|
266
|
+
* already terminal, and enforces the turn contract otherwise: a still-running
|
|
267
|
+
* bound task (or a still-running task at this session's reply address, for
|
|
268
|
+
* conversational continuations that never got their own bound session)
|
|
269
|
+
* escalates immediately to a surfaced human question instead of waiting out
|
|
270
|
+
* the stall window, because the agent finished without recording an outcome.
|
|
271
|
+
* When the bound session matches the persisted execution, that question's work input includes
|
|
272
|
+
* `systemReason: "unresolved_turn"` and `completedSessionId`. Application recovery can distinguish
|
|
273
|
+
* this completed execution from an agent's genuine request for human input.
|
|
1182
274
|
*
|
|
1183
275
|
* Every store call is swallowed: a hook throw fails the turn.
|
|
276
|
+
* Transcript projection writes at session response boundaries are likewise
|
|
277
|
+
* best-effort; the API can rebuild a missing projection from durable events.
|
|
278
|
+
*
|
|
279
|
+
* Export the returned `factory` hook from the target agent's `hooks/` directory. The hook reads
|
|
280
|
+
* the Task ID and attempt written by `withFactoryTask`; late events from replaced attempts cannot
|
|
281
|
+
* advance current work. Provider failure recovery requeues below `maxAttempts` and fails the Task
|
|
282
|
+
* at the limit. A clean turn that records no outcome creates a human question instead of silently
|
|
283
|
+
* retrying the same brief.
|
|
284
|
+
*
|
|
285
|
+
* @param stores - Task store used for usage, recovery, cancellation, and completion projection.
|
|
286
|
+
* @param options - Attempt limits plus application-owned question and conversation callbacks.
|
|
287
|
+
* @returns An Eve hook definition under the `factory` property.
|
|
288
|
+
* @see The shipped `docs/recipes/retry-recovery.md` recipe for bounded replacement attempts.
|
|
289
|
+
*
|
|
290
|
+
* @example
|
|
291
|
+
* ```ts
|
|
292
|
+
* import { createFactoryHooks, type FactoryHooksStores } from "@vercel/factory";
|
|
293
|
+
*
|
|
294
|
+
* declare const stores: FactoryHooksStores;
|
|
295
|
+
* const hooks = createFactoryHooks(stores, {
|
|
296
|
+
* presentQuestion: async (questionTask) => {
|
|
297
|
+
* // Deliver the persisted question through the application's operator channel.
|
|
298
|
+
* },
|
|
299
|
+
* });
|
|
300
|
+
* export default hooks.factory;
|
|
301
|
+
* ```
|
|
1184
302
|
*/
|
|
1185
|
-
function
|
|
303
|
+
function createFactoryHooks(stores, options) {
|
|
1186
304
|
const taskIdAttribute = options.taskIdAttribute ?? "factoryTaskId";
|
|
305
|
+
const taskAttemptAttribute = options.taskAttemptAttribute ?? "factoryTaskAttempt";
|
|
306
|
+
const maxAttempts = z.number().int().min(1).parse(options.maxAttempts ?? 3);
|
|
1187
307
|
function boundTaskId(ctx) {
|
|
1188
308
|
const parsed = taskIdSchema.safeParse(ctx.session.auth.initiator?.attributes[taskIdAttribute]);
|
|
1189
309
|
return parsed.success ? parsed.data : void 0;
|
|
1190
310
|
}
|
|
311
|
+
function boundExecution(ctx) {
|
|
312
|
+
const attributes = ctx.session.auth.initiator?.attributes;
|
|
313
|
+
const taskId = boundTaskId(ctx);
|
|
314
|
+
const attempt = z.coerce.number().int().min(1).safeParse(attributes?.[taskAttemptAttribute]);
|
|
315
|
+
return taskId !== void 0 && attempt.success ? {
|
|
316
|
+
taskId,
|
|
317
|
+
attempt: attempt.data
|
|
318
|
+
} : void 0;
|
|
319
|
+
}
|
|
320
|
+
async function recoverBoundResult(task, ctx) {
|
|
321
|
+
const binding = boundExecution(ctx);
|
|
322
|
+
if (binding?.taskId !== task.id || binding.attempt !== task.attempt || task.execution?.provider !== "eve" || task.execution.sessionId !== ctx.session.id) return false;
|
|
323
|
+
return await options.recoverResult?.(task) ?? false;
|
|
324
|
+
}
|
|
325
|
+
async function recoverFailedAttempt(binding, failure, ctx) {
|
|
326
|
+
const task = await stores.tasks.get(binding.taskId);
|
|
327
|
+
if (!task || task.attempt !== binding.attempt || options.recoverResult && task.execution && (task.execution.provider !== "eve" || task.execution.sessionId !== ctx.session.id)) return;
|
|
328
|
+
if (await recoverBoundResult(task, ctx)) {
|
|
329
|
+
await settleIfTerminal(task.id);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (task.state !== "running") return;
|
|
333
|
+
const exhausted = task.attempt >= maxAttempts;
|
|
334
|
+
await stores.tasks.transition(task.id, exhausted ? "failed" : "queued", {
|
|
335
|
+
expectFrom: "running",
|
|
336
|
+
expectAttempt: binding.attempt,
|
|
337
|
+
phase: "retry",
|
|
338
|
+
reason: exhausted ? `${failure} on attempt ${task.attempt} of ${maxAttempts}; attempts exhausted` : `${failure} on attempt ${task.attempt} of ${maxAttempts}; replacement queued`
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
async function cancelAttempt(binding, reason) {
|
|
342
|
+
const task = await stores.tasks.get(binding.taskId);
|
|
343
|
+
if (task?.state !== "running" || task.attempt !== binding.attempt) return;
|
|
344
|
+
await stores.tasks.transition(task.id, "cancelled", {
|
|
345
|
+
expectFrom: "running",
|
|
346
|
+
expectAttempt: binding.attempt,
|
|
347
|
+
phase: "execution",
|
|
348
|
+
reason
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
async function isConversation(task) {
|
|
352
|
+
try {
|
|
353
|
+
return await options.isConversation?.(task) ?? false;
|
|
354
|
+
} catch {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
1191
358
|
async function settleIfTerminal(taskId) {
|
|
1192
359
|
const task = await stores.tasks.get(taskId);
|
|
1193
360
|
if (task?.usage === void 0 || task.settledUsd !== void 0 || !terminalTaskStates.has(task.state)) return;
|
|
1194
|
-
await stores.tasks.settle(task.id, {
|
|
361
|
+
await stores.tasks.settle(task.id, {
|
|
362
|
+
cost: { totalUsd: task.usage.costUsd },
|
|
363
|
+
reason: "measured usage settled at turn end"
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
async function escalateUnresolvedTurn(task, completedSessionId) {
|
|
367
|
+
await createAndPresentQuestion(stores, task, UNRESOLVED_TURN_QUESTION, options.presentQuestion, completedSessionId).catch(() => void 0);
|
|
1195
368
|
}
|
|
1196
369
|
return { factory: defineHook({ events: {
|
|
1197
370
|
async "step.completed"(event, ctx) {
|
|
1198
371
|
const taskId = boundTaskId(ctx);
|
|
1199
372
|
const usage = event.data.usage;
|
|
1200
373
|
if (taskId === void 0 || usage === void 0) return;
|
|
1201
|
-
|
|
1202
|
-
|
|
374
|
+
const binding = boundExecution(ctx);
|
|
375
|
+
if (binding !== void 0) {
|
|
376
|
+
if ((await stores.tasks.get(taskId).catch(() => null))?.attempt !== binding.attempt) return;
|
|
377
|
+
}
|
|
378
|
+
await stores.tasks.recordUsage(taskId, { step: {
|
|
379
|
+
turnId: JSON.stringify([ctx.session.id, event.data.turnId]),
|
|
1203
380
|
stepIndex: event.data.stepIndex,
|
|
1204
381
|
...usage
|
|
1205
|
-
}).catch(() => void 0);
|
|
382
|
+
} }).catch(() => void 0);
|
|
383
|
+
},
|
|
384
|
+
async "turn.failed"(event, ctx) {
|
|
385
|
+
const binding = boundExecution(ctx);
|
|
386
|
+
if (binding !== void 0) {
|
|
387
|
+
await recoverFailedAttempt(binding, `provider turn failed (${event.data.code}): ${event.data.message}`, ctx).catch(() => void 0);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const taskId = boundTaskId(ctx);
|
|
391
|
+
const task = taskId ? await stores.tasks.get(taskId).catch(() => null) : null;
|
|
392
|
+
if (task?.state === "running" && await isConversation(task)) {
|
|
393
|
+
await stores.tasks.transition(task.id, "failed", {
|
|
394
|
+
expectFrom: "running",
|
|
395
|
+
reason: "Conversation turn failed"
|
|
396
|
+
}).catch(() => void 0);
|
|
397
|
+
await settleIfTerminal(task.id).catch(() => void 0);
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
async "session.failed"(event, ctx) {
|
|
401
|
+
const binding = boundExecution(ctx);
|
|
402
|
+
if (binding !== void 0) await recoverFailedAttempt(binding, `provider session failed (${event.data.code}): ${event.data.message}`, ctx).catch(() => void 0);
|
|
403
|
+
await options.materializeTranscript?.(materializationInput(ctx)).catch(() => void 0);
|
|
404
|
+
},
|
|
405
|
+
async "session.waiting"(_event, ctx) {
|
|
406
|
+
await options.materializeTranscript?.(materializationInput(ctx)).catch(() => void 0);
|
|
407
|
+
},
|
|
408
|
+
async "session.completed"(_event, ctx) {
|
|
409
|
+
await options.materializeTranscript?.(materializationInput(ctx)).catch(() => void 0);
|
|
410
|
+
},
|
|
411
|
+
async "turn.cancelled"(_event, ctx) {
|
|
412
|
+
const binding = boundExecution(ctx);
|
|
413
|
+
if (binding !== void 0) {
|
|
414
|
+
await cancelAttempt(binding, `provider turn cancelled on attempt ${binding.attempt}`).catch(() => void 0);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const taskId = boundTaskId(ctx);
|
|
418
|
+
const task = taskId ? await stores.tasks.get(taskId).catch(() => null) : null;
|
|
419
|
+
if (task && await isConversation(task)) {
|
|
420
|
+
if (task.state === "running") await stores.tasks.transition(task.id, "cancelled", {
|
|
421
|
+
expectFrom: "running",
|
|
422
|
+
reason: "Conversation turn interrupted"
|
|
423
|
+
}).catch(() => void 0);
|
|
424
|
+
await settleIfTerminal(task.id).catch(() => void 0);
|
|
425
|
+
}
|
|
1206
426
|
},
|
|
1207
427
|
async "turn.completed"(_event, ctx) {
|
|
1208
428
|
const taskId = boundTaskId(ctx);
|
|
1209
|
-
|
|
429
|
+
const binding = boundExecution(ctx);
|
|
430
|
+
if (taskId !== void 0) {
|
|
431
|
+
const task = await stores.tasks.get(taskId).catch(() => null);
|
|
432
|
+
if (binding !== void 0 && task?.attempt !== binding.attempt) return;
|
|
433
|
+
if (binding && options.recoverResult && task?.execution && (task.execution.provider !== "eve" || task.execution.sessionId !== ctx.session.id)) return;
|
|
434
|
+
let recovered;
|
|
435
|
+
try {
|
|
436
|
+
recovered = task !== null && await recoverBoundResult(task, ctx);
|
|
437
|
+
} catch {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (!recovered && task?.state === "running" && !task.waitingForMessages) {
|
|
441
|
+
const completion = await options.completeConversation?.(task).catch(() => false);
|
|
442
|
+
if (completion) await stores.tasks.transition(taskId, "succeeded", {
|
|
443
|
+
expectFrom: "running",
|
|
444
|
+
expectAttempt: task.attempt,
|
|
445
|
+
reason: "Conversation turn completed",
|
|
446
|
+
output: completion.output
|
|
447
|
+
}).catch(() => void 0);
|
|
448
|
+
else await escalateUnresolvedTurn(task, binding && task.execution?.provider === "eve" && task.execution.sessionId === ctx.session.id ? ctx.session.id : void 0);
|
|
449
|
+
}
|
|
450
|
+
await settleIfTerminal(taskId).catch(() => void 0);
|
|
451
|
+
}
|
|
1210
452
|
const address = ctx.channel.continuationToken;
|
|
1211
453
|
if (address === void 0) return;
|
|
1212
|
-
const abandoned = (await stores.tasks.list().catch(() => [])).filter((task) => task.state === "running" && task.replyTo.address === address);
|
|
1213
|
-
for (const task of abandoned) await
|
|
1214
|
-
text: "The worker stopped without recording an outcome. Please review the task and reply when Factory should retry it.",
|
|
1215
|
-
options: ["Retry the task"]
|
|
1216
|
-
}, options.presentQuestion).catch(() => void 0);
|
|
454
|
+
const abandoned = (await stores.tasks.list().catch(() => [])).filter((task) => task.id !== taskId && task.state === "running" && !task.waitingForMessages && task.replyTo.address === address);
|
|
455
|
+
for (const task of abandoned) await escalateUnresolvedTurn(task);
|
|
1217
456
|
}
|
|
1218
457
|
} }) };
|
|
1219
458
|
}
|
|
1220
459
|
//#endregion
|
|
1221
|
-
export {
|
|
460
|
+
export { cancelEveSession, conversationBinding, conversationBindingForSession, conversationHeaders, createDelegateAgentTool, createEveTranscriptMaterializer, createFactoryHooks, createInvokeAgentTool, createTaskTools, createTranscriptSnapshotHook, factoryReplyAddressAttribute, factoryReplyAddressHeader, factoryReplyChannelAttribute, factoryReplyChannelHeader, factoryRepositoryIdsAttribute, factoryRepositoryIdsHeader, factoryTaskAttemptAttribute, factoryTaskAttemptHeader, factoryTaskAttribute, factoryTaskHeader, forwardEveSessionRequest, hasFactoryTaskBinding, optionalTaskIdForSession, readCompletedEveTurn, replyToForSession, repositoryIdsForSession, requireTaskIdForSession, startEveSession, taskBindingForSession, taskHeaders, taskSessionAuth, withFactoryTask, withOptionalFactoryTask };
|
|
1222
461
|
|
|
1223
462
|
//# sourceMappingURL=index.mjs.map
|