@herdctl/core 0.0.1
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/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-test.log +219 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/coverage-final.json +51 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +251 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/src/config/index.html +191 -0
- package/coverage/src/config/index.ts.html +442 -0
- package/coverage/src/config/interpolate.ts.html +652 -0
- package/coverage/src/config/loader.ts.html +1501 -0
- package/coverage/src/config/merge.ts.html +823 -0
- package/coverage/src/config/parser.ts.html +1213 -0
- package/coverage/src/config/schema.ts.html +1123 -0
- package/coverage/src/fleet-manager/errors.ts.html +2326 -0
- package/coverage/src/fleet-manager/event-types.ts.html +1219 -0
- package/coverage/src/fleet-manager/fleet-manager.ts.html +7030 -0
- package/coverage/src/fleet-manager/index.html +206 -0
- package/coverage/src/fleet-manager/index.ts.html +469 -0
- package/coverage/src/fleet-manager/job-manager.ts.html +2074 -0
- package/coverage/src/fleet-manager/job-queue.ts.html +2479 -0
- package/coverage/src/fleet-manager/types.ts.html +2602 -0
- package/coverage/src/index.html +116 -0
- package/coverage/src/index.ts.html +181 -0
- package/coverage/src/runner/errors.ts.html +1006 -0
- package/coverage/src/runner/index.html +191 -0
- package/coverage/src/runner/index.ts.html +256 -0
- package/coverage/src/runner/job-executor.ts.html +1429 -0
- package/coverage/src/runner/message-processor.ts.html +1150 -0
- package/coverage/src/runner/sdk-adapter.ts.html +658 -0
- package/coverage/src/runner/types.ts.html +559 -0
- package/coverage/src/scheduler/errors.ts.html +388 -0
- package/coverage/src/scheduler/index.html +206 -0
- package/coverage/src/scheduler/index.ts.html +244 -0
- package/coverage/src/scheduler/interval.ts.html +652 -0
- package/coverage/src/scheduler/schedule-runner.ts.html +1411 -0
- package/coverage/src/scheduler/schedule-state.ts.html +718 -0
- package/coverage/src/scheduler/scheduler.ts.html +1795 -0
- package/coverage/src/scheduler/types.ts.html +733 -0
- package/coverage/src/state/directory.ts.html +736 -0
- package/coverage/src/state/errors.ts.html +376 -0
- package/coverage/src/state/fleet-state.ts.html +937 -0
- package/coverage/src/state/index.html +221 -0
- package/coverage/src/state/index.ts.html +322 -0
- package/coverage/src/state/job-metadata.ts.html +1420 -0
- package/coverage/src/state/job-output.ts.html +1033 -0
- package/coverage/src/state/schemas/fleet-state.ts.html +445 -0
- package/coverage/src/state/schemas/index.html +176 -0
- package/coverage/src/state/schemas/index.ts.html +286 -0
- package/coverage/src/state/schemas/job-metadata.ts.html +628 -0
- package/coverage/src/state/schemas/job-output.ts.html +616 -0
- package/coverage/src/state/schemas/session-info.ts.html +361 -0
- package/coverage/src/state/session.ts.html +844 -0
- package/coverage/src/state/types.ts.html +262 -0
- package/coverage/src/state/utils/atomic.ts.html +748 -0
- package/coverage/src/state/utils/index.html +146 -0
- package/coverage/src/state/utils/index.ts.html +103 -0
- package/coverage/src/state/utils/reads.ts.html +1621 -0
- package/coverage/src/work-sources/adapters/github.ts.html +3583 -0
- package/coverage/src/work-sources/adapters/index.html +131 -0
- package/coverage/src/work-sources/adapters/index.ts.html +277 -0
- package/coverage/src/work-sources/errors.ts.html +298 -0
- package/coverage/src/work-sources/index.html +176 -0
- package/coverage/src/work-sources/index.ts.html +529 -0
- package/coverage/src/work-sources/manager.ts.html +1324 -0
- package/coverage/src/work-sources/registry.ts.html +619 -0
- package/coverage/src/work-sources/types.ts.html +568 -0
- package/dist/config/__tests__/agent.test.d.ts +2 -0
- package/dist/config/__tests__/agent.test.d.ts.map +1 -0
- package/dist/config/__tests__/agent.test.js +752 -0
- package/dist/config/__tests__/agent.test.js.map +1 -0
- package/dist/config/__tests__/interpolate.test.d.ts +2 -0
- package/dist/config/__tests__/interpolate.test.d.ts.map +1 -0
- package/dist/config/__tests__/interpolate.test.js +509 -0
- package/dist/config/__tests__/interpolate.test.js.map +1 -0
- package/dist/config/__tests__/loader.test.d.ts +2 -0
- package/dist/config/__tests__/loader.test.d.ts.map +1 -0
- package/dist/config/__tests__/loader.test.js +631 -0
- package/dist/config/__tests__/loader.test.js.map +1 -0
- package/dist/config/__tests__/merge.test.d.ts +2 -0
- package/dist/config/__tests__/merge.test.d.ts.map +1 -0
- package/dist/config/__tests__/merge.test.js +672 -0
- package/dist/config/__tests__/merge.test.js.map +1 -0
- package/dist/config/__tests__/parser.test.d.ts +2 -0
- package/dist/config/__tests__/parser.test.d.ts.map +1 -0
- package/dist/config/__tests__/parser.test.js +476 -0
- package/dist/config/__tests__/parser.test.js.map +1 -0
- package/dist/config/__tests__/schema.test.d.ts +2 -0
- package/dist/config/__tests__/schema.test.d.ts.map +1 -0
- package/dist/config/__tests__/schema.test.js +776 -0
- package/dist/config/__tests__/schema.test.js.map +1 -0
- package/dist/config/index.d.ts +11 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +26 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/interpolate.d.ts +76 -0
- package/dist/config/interpolate.d.ts.map +1 -0
- package/dist/config/interpolate.js +143 -0
- package/dist/config/interpolate.js.map +1 -0
- package/dist/config/loader.d.ts +147 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +336 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/merge.d.ts +84 -0
- package/dist/config/merge.d.ts.map +1 -0
- package/dist/config/merge.js +138 -0
- package/dist/config/merge.js.map +1 -0
- package/dist/config/parser.d.ts +143 -0
- package/dist/config/parser.d.ts.map +1 -0
- package/dist/config/parser.js +316 -0
- package/dist/config/parser.js.map +1 -0
- package/dist/config/schema.d.ts +1906 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +268 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/fleet-manager/__tests__/coverage.test.d.ts +13 -0
- package/dist/fleet-manager/__tests__/coverage.test.d.ts.map +1 -0
- package/dist/fleet-manager/__tests__/coverage.test.js +2282 -0
- package/dist/fleet-manager/__tests__/coverage.test.js.map +1 -0
- package/dist/fleet-manager/__tests__/errors.test.d.ts +7 -0
- package/dist/fleet-manager/__tests__/errors.test.d.ts.map +1 -0
- package/dist/fleet-manager/__tests__/errors.test.js +557 -0
- package/dist/fleet-manager/__tests__/errors.test.js.map +1 -0
- package/dist/fleet-manager/__tests__/event-helpers.test.d.ts +7 -0
- package/dist/fleet-manager/__tests__/event-helpers.test.d.ts.map +1 -0
- package/dist/fleet-manager/__tests__/event-helpers.test.js +368 -0
- package/dist/fleet-manager/__tests__/event-helpers.test.js.map +1 -0
- package/dist/fleet-manager/__tests__/integration.test.d.ts +11 -0
- package/dist/fleet-manager/__tests__/integration.test.d.ts.map +1 -0
- package/dist/fleet-manager/__tests__/integration.test.js +949 -0
- package/dist/fleet-manager/__tests__/integration.test.js.map +1 -0
- package/dist/fleet-manager/__tests__/job-control.test.d.ts +7 -0
- package/dist/fleet-manager/__tests__/job-control.test.d.ts.map +1 -0
- package/dist/fleet-manager/__tests__/job-control.test.js +215 -0
- package/dist/fleet-manager/__tests__/job-control.test.js.map +1 -0
- package/dist/fleet-manager/__tests__/job-manager.test.d.ts +7 -0
- package/dist/fleet-manager/__tests__/job-manager.test.d.ts.map +1 -0
- package/dist/fleet-manager/__tests__/job-manager.test.js +659 -0
- package/dist/fleet-manager/__tests__/job-manager.test.js.map +1 -0
- package/dist/fleet-manager/__tests__/job-queue.test.d.ts +5 -0
- package/dist/fleet-manager/__tests__/job-queue.test.d.ts.map +1 -0
- package/dist/fleet-manager/__tests__/job-queue.test.js +315 -0
- package/dist/fleet-manager/__tests__/job-queue.test.js.map +1 -0
- package/dist/fleet-manager/__tests__/reload.test.d.ts +7 -0
- package/dist/fleet-manager/__tests__/reload.test.d.ts.map +1 -0
- package/dist/fleet-manager/__tests__/reload.test.js +609 -0
- package/dist/fleet-manager/__tests__/reload.test.js.map +1 -0
- package/dist/fleet-manager/__tests__/status-queries.test.d.ts +7 -0
- package/dist/fleet-manager/__tests__/status-queries.test.d.ts.map +1 -0
- package/dist/fleet-manager/__tests__/status-queries.test.js +488 -0
- package/dist/fleet-manager/__tests__/status-queries.test.js.map +1 -0
- package/dist/fleet-manager/__tests__/trigger.test.d.ts +7 -0
- package/dist/fleet-manager/__tests__/trigger.test.d.ts.map +1 -0
- package/dist/fleet-manager/__tests__/trigger.test.js +471 -0
- package/dist/fleet-manager/__tests__/trigger.test.js.map +1 -0
- package/dist/fleet-manager/errors.d.ts +407 -0
- package/dist/fleet-manager/errors.d.ts.map +1 -0
- package/dist/fleet-manager/errors.js +569 -0
- package/dist/fleet-manager/errors.js.map +1 -0
- package/dist/fleet-manager/event-types.d.ts +302 -0
- package/dist/fleet-manager/event-types.d.ts.map +1 -0
- package/dist/fleet-manager/event-types.js +9 -0
- package/dist/fleet-manager/event-types.js.map +1 -0
- package/dist/fleet-manager/fleet-manager.d.ts +699 -0
- package/dist/fleet-manager/fleet-manager.d.ts.map +1 -0
- package/dist/fleet-manager/fleet-manager.js +1906 -0
- package/dist/fleet-manager/fleet-manager.js.map +1 -0
- package/dist/fleet-manager/index.d.ts +17 -0
- package/dist/fleet-manager/index.d.ts.map +1 -0
- package/dist/fleet-manager/index.js +29 -0
- package/dist/fleet-manager/index.js.map +1 -0
- package/dist/fleet-manager/job-manager.d.ts +271 -0
- package/dist/fleet-manager/job-manager.d.ts.map +1 -0
- package/dist/fleet-manager/job-manager.js +443 -0
- package/dist/fleet-manager/job-manager.js.map +1 -0
- package/dist/fleet-manager/job-queue.d.ts +422 -0
- package/dist/fleet-manager/job-queue.d.ts.map +1 -0
- package/dist/fleet-manager/job-queue.js +448 -0
- package/dist/fleet-manager/job-queue.js.map +1 -0
- package/dist/fleet-manager/types.d.ts +680 -0
- package/dist/fleet-manager/types.d.ts.map +1 -0
- package/dist/fleet-manager/types.js +8 -0
- package/dist/fleet-manager/types.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/runner/__tests__/errors.test.d.ts +2 -0
- package/dist/runner/__tests__/errors.test.d.ts.map +1 -0
- package/dist/runner/__tests__/errors.test.js +264 -0
- package/dist/runner/__tests__/errors.test.js.map +1 -0
- package/dist/runner/__tests__/job-executor.test.d.ts +2 -0
- package/dist/runner/__tests__/job-executor.test.d.ts.map +1 -0
- package/dist/runner/__tests__/job-executor.test.js +1345 -0
- package/dist/runner/__tests__/job-executor.test.js.map +1 -0
- package/dist/runner/__tests__/message-processor.test.d.ts +2 -0
- package/dist/runner/__tests__/message-processor.test.d.ts.map +1 -0
- package/dist/runner/__tests__/message-processor.test.js +768 -0
- package/dist/runner/__tests__/message-processor.test.js.map +1 -0
- package/dist/runner/__tests__/sdk-adapter.test.d.ts +2 -0
- package/dist/runner/__tests__/sdk-adapter.test.d.ts.map +1 -0
- package/dist/runner/__tests__/sdk-adapter.test.js +554 -0
- package/dist/runner/__tests__/sdk-adapter.test.js.map +1 -0
- package/dist/runner/errors.d.ts +121 -0
- package/dist/runner/errors.d.ts.map +1 -0
- package/dist/runner/errors.js +212 -0
- package/dist/runner/errors.js.map +1 -0
- package/dist/runner/index.d.ts +12 -0
- package/dist/runner/index.d.ts.map +1 -0
- package/dist/runner/index.js +15 -0
- package/dist/runner/index.js.map +1 -0
- package/dist/runner/job-executor.d.ts +98 -0
- package/dist/runner/job-executor.d.ts.map +1 -0
- package/dist/runner/job-executor.js +333 -0
- package/dist/runner/job-executor.js.map +1 -0
- package/dist/runner/message-processor.d.ts +45 -0
- package/dist/runner/message-processor.d.ts.map +1 -0
- package/dist/runner/message-processor.js +294 -0
- package/dist/runner/message-processor.js.map +1 -0
- package/dist/runner/sdk-adapter.d.ts +60 -0
- package/dist/runner/sdk-adapter.d.ts.map +1 -0
- package/dist/runner/sdk-adapter.js +138 -0
- package/dist/runner/sdk-adapter.js.map +1 -0
- package/dist/runner/types.d.ts +135 -0
- package/dist/runner/types.d.ts.map +1 -0
- package/dist/runner/types.js +7 -0
- package/dist/runner/types.js.map +1 -0
- package/dist/scheduler/__tests__/errors.test.d.ts +2 -0
- package/dist/scheduler/__tests__/errors.test.d.ts.map +1 -0
- package/dist/scheduler/__tests__/errors.test.js +101 -0
- package/dist/scheduler/__tests__/errors.test.js.map +1 -0
- package/dist/scheduler/__tests__/interval.test.d.ts +2 -0
- package/dist/scheduler/__tests__/interval.test.d.ts.map +1 -0
- package/dist/scheduler/__tests__/interval.test.js +419 -0
- package/dist/scheduler/__tests__/interval.test.js.map +1 -0
- package/dist/scheduler/__tests__/schedule-runner.test.d.ts +2 -0
- package/dist/scheduler/__tests__/schedule-runner.test.d.ts.map +1 -0
- package/dist/scheduler/__tests__/schedule-runner.test.js +634 -0
- package/dist/scheduler/__tests__/schedule-runner.test.js.map +1 -0
- package/dist/scheduler/__tests__/schedule-state.test.d.ts +2 -0
- package/dist/scheduler/__tests__/schedule-state.test.d.ts.map +1 -0
- package/dist/scheduler/__tests__/schedule-state.test.js +572 -0
- package/dist/scheduler/__tests__/schedule-state.test.js.map +1 -0
- package/dist/scheduler/__tests__/scheduler.test.d.ts +2 -0
- package/dist/scheduler/__tests__/scheduler.test.d.ts.map +1 -0
- package/dist/scheduler/__tests__/scheduler.test.js +987 -0
- package/dist/scheduler/__tests__/scheduler.test.js.map +1 -0
- package/dist/scheduler/errors.d.ts +61 -0
- package/dist/scheduler/errors.d.ts.map +1 -0
- package/dist/scheduler/errors.js +81 -0
- package/dist/scheduler/errors.js.map +1 -0
- package/dist/scheduler/index.d.ts +13 -0
- package/dist/scheduler/index.d.ts.map +1 -0
- package/dist/scheduler/index.js +17 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/scheduler/interval.d.ts +64 -0
- package/dist/scheduler/interval.d.ts.map +1 -0
- package/dist/scheduler/interval.js +139 -0
- package/dist/scheduler/interval.js.map +1 -0
- package/dist/scheduler/schedule-runner.d.ts +149 -0
- package/dist/scheduler/schedule-runner.d.ts.map +1 -0
- package/dist/scheduler/schedule-runner.js +277 -0
- package/dist/scheduler/schedule-runner.js.map +1 -0
- package/dist/scheduler/schedule-state.d.ts +105 -0
- package/dist/scheduler/schedule-state.d.ts.map +1 -0
- package/dist/scheduler/schedule-state.js +151 -0
- package/dist/scheduler/schedule-state.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +138 -0
- package/dist/scheduler/scheduler.d.ts.map +1 -0
- package/dist/scheduler/scheduler.js +423 -0
- package/dist/scheduler/scheduler.js.map +1 -0
- package/dist/scheduler/types.d.ts +160 -0
- package/dist/scheduler/types.d.ts.map +1 -0
- package/dist/scheduler/types.js +8 -0
- package/dist/scheduler/types.js.map +1 -0
- package/dist/state/__tests__/directory.test.d.ts +2 -0
- package/dist/state/__tests__/directory.test.d.ts.map +1 -0
- package/dist/state/__tests__/directory.test.js +414 -0
- package/dist/state/__tests__/directory.test.js.map +1 -0
- package/dist/state/__tests__/fleet-state.test.d.ts +2 -0
- package/dist/state/__tests__/fleet-state.test.d.ts.map +1 -0
- package/dist/state/__tests__/fleet-state.test.js +696 -0
- package/dist/state/__tests__/fleet-state.test.js.map +1 -0
- package/dist/state/__tests__/job-metadata-schema.test.d.ts +2 -0
- package/dist/state/__tests__/job-metadata-schema.test.d.ts.map +1 -0
- package/dist/state/__tests__/job-metadata-schema.test.js +329 -0
- package/dist/state/__tests__/job-metadata-schema.test.js.map +1 -0
- package/dist/state/__tests__/job-metadata.test.d.ts +2 -0
- package/dist/state/__tests__/job-metadata.test.d.ts.map +1 -0
- package/dist/state/__tests__/job-metadata.test.js +667 -0
- package/dist/state/__tests__/job-metadata.test.js.map +1 -0
- package/dist/state/__tests__/job-output.test.d.ts +2 -0
- package/dist/state/__tests__/job-output.test.d.ts.map +1 -0
- package/dist/state/__tests__/job-output.test.js +672 -0
- package/dist/state/__tests__/job-output.test.js.map +1 -0
- package/dist/state/__tests__/session-schema.test.d.ts +2 -0
- package/dist/state/__tests__/session-schema.test.d.ts.map +1 -0
- package/dist/state/__tests__/session-schema.test.js +323 -0
- package/dist/state/__tests__/session-schema.test.js.map +1 -0
- package/dist/state/__tests__/session.test.d.ts +2 -0
- package/dist/state/__tests__/session.test.d.ts.map +1 -0
- package/dist/state/__tests__/session.test.js +468 -0
- package/dist/state/__tests__/session.test.js.map +1 -0
- package/dist/state/directory.d.ts +42 -0
- package/dist/state/directory.d.ts.map +1 -0
- package/dist/state/directory.js +170 -0
- package/dist/state/directory.js.map +1 -0
- package/dist/state/errors.d.ts +44 -0
- package/dist/state/errors.d.ts.map +1 -0
- package/dist/state/errors.js +82 -0
- package/dist/state/errors.js.map +1 -0
- package/dist/state/fleet-state.d.ts +126 -0
- package/dist/state/fleet-state.d.ts.map +1 -0
- package/dist/state/fleet-state.js +196 -0
- package/dist/state/fleet-state.js.map +1 -0
- package/dist/state/index.d.ts +21 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +30 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/job-metadata.d.ts +151 -0
- package/dist/state/job-metadata.d.ts.map +1 -0
- package/dist/state/job-metadata.js +287 -0
- package/dist/state/job-metadata.js.map +1 -0
- package/dist/state/job-output.d.ts +116 -0
- package/dist/state/job-output.d.ts.map +1 -0
- package/dist/state/job-output.js +218 -0
- package/dist/state/job-output.js.map +1 -0
- package/dist/state/schemas/__tests__/job-output.test.d.ts +2 -0
- package/dist/state/schemas/__tests__/job-output.test.d.ts.map +1 -0
- package/dist/state/schemas/__tests__/job-output.test.js +279 -0
- package/dist/state/schemas/__tests__/job-output.test.js.map +1 -0
- package/dist/state/schemas/fleet-state.d.ts +249 -0
- package/dist/state/schemas/fleet-state.d.ts.map +1 -0
- package/dist/state/schemas/fleet-state.js +97 -0
- package/dist/state/schemas/fleet-state.js.map +1 -0
- package/dist/state/schemas/index.d.ts +10 -0
- package/dist/state/schemas/index.d.ts.map +1 -0
- package/dist/state/schemas/index.js +10 -0
- package/dist/state/schemas/index.js.map +1 -0
- package/dist/state/schemas/job-metadata.d.ts +118 -0
- package/dist/state/schemas/job-metadata.d.ts.map +1 -0
- package/dist/state/schemas/job-metadata.js +123 -0
- package/dist/state/schemas/job-metadata.js.map +1 -0
- package/dist/state/schemas/job-output.d.ts +291 -0
- package/dist/state/schemas/job-output.d.ts.map +1 -0
- package/dist/state/schemas/job-output.js +132 -0
- package/dist/state/schemas/job-output.js.map +1 -0
- package/dist/state/schemas/session-info.d.ts +65 -0
- package/dist/state/schemas/session-info.d.ts.map +1 -0
- package/dist/state/schemas/session-info.js +58 -0
- package/dist/state/schemas/session-info.js.map +1 -0
- package/dist/state/session.d.ts +92 -0
- package/dist/state/session.d.ts.map +1 -0
- package/dist/state/session.js +173 -0
- package/dist/state/session.js.map +1 -0
- package/dist/state/types.d.ts +54 -0
- package/dist/state/types.d.ts.map +1 -0
- package/dist/state/types.js +18 -0
- package/dist/state/types.js.map +1 -0
- package/dist/state/utils/__tests__/atomic.test.d.ts +2 -0
- package/dist/state/utils/__tests__/atomic.test.d.ts.map +1 -0
- package/dist/state/utils/__tests__/atomic.test.js +537 -0
- package/dist/state/utils/__tests__/atomic.test.js.map +1 -0
- package/dist/state/utils/__tests__/reads.test.d.ts +2 -0
- package/dist/state/utils/__tests__/reads.test.d.ts.map +1 -0
- package/dist/state/utils/__tests__/reads.test.js +792 -0
- package/dist/state/utils/__tests__/reads.test.js.map +1 -0
- package/dist/state/utils/atomic.d.ts +89 -0
- package/dist/state/utils/atomic.d.ts.map +1 -0
- package/dist/state/utils/atomic.js +157 -0
- package/dist/state/utils/atomic.js.map +1 -0
- package/dist/state/utils/index.d.ts +6 -0
- package/dist/state/utils/index.d.ts.map +1 -0
- package/dist/state/utils/index.js +6 -0
- package/dist/state/utils/index.js.map +1 -0
- package/dist/state/utils/reads.d.ts +196 -0
- package/dist/state/utils/reads.d.ts.map +1 -0
- package/dist/state/utils/reads.js +346 -0
- package/dist/state/utils/reads.js.map +1 -0
- package/dist/work-sources/__tests__/github.test.d.ts +2 -0
- package/dist/work-sources/__tests__/github.test.d.ts.map +1 -0
- package/dist/work-sources/__tests__/github.test.js +1334 -0
- package/dist/work-sources/__tests__/github.test.js.map +1 -0
- package/dist/work-sources/__tests__/manager.test.d.ts +2 -0
- package/dist/work-sources/__tests__/manager.test.d.ts.map +1 -0
- package/dist/work-sources/__tests__/manager.test.js +424 -0
- package/dist/work-sources/__tests__/manager.test.js.map +1 -0
- package/dist/work-sources/__tests__/registry.test.d.ts +2 -0
- package/dist/work-sources/__tests__/registry.test.d.ts.map +1 -0
- package/dist/work-sources/__tests__/registry.test.js +381 -0
- package/dist/work-sources/__tests__/registry.test.js.map +1 -0
- package/dist/work-sources/__tests__/types.test.d.ts +2 -0
- package/dist/work-sources/__tests__/types.test.d.ts.map +1 -0
- package/dist/work-sources/__tests__/types.test.js +406 -0
- package/dist/work-sources/__tests__/types.test.js.map +1 -0
- package/dist/work-sources/adapters/github.d.ts +290 -0
- package/dist/work-sources/adapters/github.d.ts.map +1 -0
- package/dist/work-sources/adapters/github.js +803 -0
- package/dist/work-sources/adapters/github.js.map +1 -0
- package/dist/work-sources/adapters/index.d.ts +10 -0
- package/dist/work-sources/adapters/index.d.ts.map +1 -0
- package/dist/work-sources/adapters/index.js +31 -0
- package/dist/work-sources/adapters/index.js.map +1 -0
- package/dist/work-sources/errors.d.ts +40 -0
- package/dist/work-sources/errors.d.ts.map +1 -0
- package/dist/work-sources/errors.js +54 -0
- package/dist/work-sources/errors.js.map +1 -0
- package/dist/work-sources/index.d.ts +105 -0
- package/dist/work-sources/index.d.ts.map +1 -0
- package/dist/work-sources/index.js +24 -0
- package/dist/work-sources/index.js.map +1 -0
- package/dist/work-sources/manager.d.ts +370 -0
- package/dist/work-sources/manager.d.ts.map +1 -0
- package/dist/work-sources/manager.js +61 -0
- package/dist/work-sources/manager.js.map +1 -0
- package/dist/work-sources/registry.d.ts +128 -0
- package/dist/work-sources/registry.d.ts.map +1 -0
- package/dist/work-sources/registry.js +132 -0
- package/dist/work-sources/registry.js.map +1 -0
- package/dist/work-sources/types.d.ts +127 -0
- package/dist/work-sources/types.d.ts.map +1 -0
- package/dist/work-sources/types.js +8 -0
- package/dist/work-sources/types.js.map +1 -0
- package/package.json +23 -0
- package/src/config/__tests__/agent.test.ts +864 -0
- package/src/config/__tests__/interpolate.test.ts +644 -0
- package/src/config/__tests__/loader.test.ts +784 -0
- package/src/config/__tests__/merge.test.ts +751 -0
- package/src/config/__tests__/parser.test.ts +533 -0
- package/src/config/__tests__/schema.test.ts +873 -0
- package/src/config/index.ts +119 -0
- package/src/config/interpolate.ts +189 -0
- package/src/config/loader.ts +472 -0
- package/src/config/merge.ts +246 -0
- package/src/config/parser.ts +376 -0
- package/src/config/schema.ts +346 -0
- package/src/fleet-manager/__tests__/coverage.test.ts +2869 -0
- package/src/fleet-manager/__tests__/errors.test.ts +660 -0
- package/src/fleet-manager/__tests__/event-helpers.test.ts +448 -0
- package/src/fleet-manager/__tests__/integration.test.ts +1209 -0
- package/src/fleet-manager/__tests__/job-control.test.ts +283 -0
- package/src/fleet-manager/__tests__/job-manager.test.ts +869 -0
- package/src/fleet-manager/__tests__/job-queue.test.ts +401 -0
- package/src/fleet-manager/__tests__/reload.test.ts +751 -0
- package/src/fleet-manager/__tests__/status-queries.test.ts +595 -0
- package/src/fleet-manager/__tests__/trigger.test.ts +601 -0
- package/src/fleet-manager/errors.ts +747 -0
- package/src/fleet-manager/event-types.ts +378 -0
- package/src/fleet-manager/fleet-manager.ts +2315 -0
- package/src/fleet-manager/index.ts +128 -0
- package/src/fleet-manager/job-manager.ts +663 -0
- package/src/fleet-manager/job-queue.ts +798 -0
- package/src/fleet-manager/types.ts +839 -0
- package/src/index.ts +32 -0
- package/src/runner/__tests__/errors.test.ts +382 -0
- package/src/runner/__tests__/job-executor.test.ts +1708 -0
- package/src/runner/__tests__/message-processor.test.ts +960 -0
- package/src/runner/__tests__/sdk-adapter.test.ts +626 -0
- package/src/runner/errors.ts +307 -0
- package/src/runner/index.ts +57 -0
- package/src/runner/job-executor.ts +448 -0
- package/src/runner/message-processor.ts +355 -0
- package/src/runner/sdk-adapter.ts +191 -0
- package/src/runner/types.ts +158 -0
- package/src/scheduler/__tests__/errors.test.ts +159 -0
- package/src/scheduler/__tests__/interval.test.ts +515 -0
- package/src/scheduler/__tests__/schedule-runner.test.ts +798 -0
- package/src/scheduler/__tests__/schedule-state.test.ts +671 -0
- package/src/scheduler/__tests__/scheduler.test.ts +1280 -0
- package/src/scheduler/errors.ts +101 -0
- package/src/scheduler/index.ts +53 -0
- package/src/scheduler/interval.ts +189 -0
- package/src/scheduler/schedule-runner.ts +442 -0
- package/src/scheduler/schedule-state.ts +211 -0
- package/src/scheduler/scheduler.ts +570 -0
- package/src/scheduler/types.ts +216 -0
- package/src/state/__tests__/directory.test.ts +595 -0
- package/src/state/__tests__/fleet-state.test.ts +868 -0
- package/src/state/__tests__/job-metadata-schema.test.ts +414 -0
- package/src/state/__tests__/job-metadata.test.ts +831 -0
- package/src/state/__tests__/job-output.test.ts +856 -0
- package/src/state/__tests__/session-schema.test.ts +378 -0
- package/src/state/__tests__/session.test.ts +604 -0
- package/src/state/directory.ts +217 -0
- package/src/state/errors.ts +97 -0
- package/src/state/fleet-state.ts +284 -0
- package/src/state/index.ts +79 -0
- package/src/state/job-metadata.ts +445 -0
- package/src/state/job-output.ts +316 -0
- package/src/state/schemas/__tests__/job-output.test.ts +338 -0
- package/src/state/schemas/fleet-state.ts +120 -0
- package/src/state/schemas/index.ts +67 -0
- package/src/state/schemas/job-metadata.ts +181 -0
- package/src/state/schemas/job-output.ts +177 -0
- package/src/state/schemas/session-info.ts +92 -0
- package/src/state/session.ts +253 -0
- package/src/state/types.ts +59 -0
- package/src/state/utils/__tests__/atomic.test.ts +723 -0
- package/src/state/utils/__tests__/reads.test.ts +1071 -0
- package/src/state/utils/atomic.ts +221 -0
- package/src/state/utils/index.ts +6 -0
- package/src/state/utils/reads.ts +512 -0
- package/src/work-sources/__tests__/github.test.ts +1800 -0
- package/src/work-sources/__tests__/manager.test.ts +529 -0
- package/src/work-sources/__tests__/registry.test.ts +477 -0
- package/src/work-sources/__tests__/types.test.ts +479 -0
- package/src/work-sources/adapters/github.ts +1166 -0
- package/src/work-sources/adapters/index.ts +64 -0
- package/src/work-sources/errors.ts +71 -0
- package/src/work-sources/index.ts +148 -0
- package/src/work-sources/manager.ts +413 -0
- package/src/work-sources/registry.ts +178 -0
- package/src/work-sources/types.ts +161 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +19 -0
|
@@ -0,0 +1,1906 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FleetManager class for library consumers
|
|
3
|
+
*
|
|
4
|
+
* Provides a simple, high-level API to initialize and run a fleet of agents
|
|
5
|
+
* with minimal configuration. Handles config loading, state directory setup,
|
|
6
|
+
* and scheduler orchestration internally.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { FleetManager } from '@herdctl/core';
|
|
11
|
+
*
|
|
12
|
+
* const manager = new FleetManager({
|
|
13
|
+
* configPath: './herdctl.yaml',
|
|
14
|
+
* stateDir: './.herdctl',
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* await manager.initialize();
|
|
18
|
+
* await manager.start();
|
|
19
|
+
*
|
|
20
|
+
* // Later...
|
|
21
|
+
* await manager.stop();
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
import { EventEmitter } from "node:events";
|
|
25
|
+
import { resolve } from "node:path";
|
|
26
|
+
import { loadConfig, ConfigNotFoundError, ConfigError, } from "../config/index.js";
|
|
27
|
+
import { initStateDirectory, createJob, getJob, updateJob, listJobs } from "../state/index.js";
|
|
28
|
+
import { Scheduler } from "../scheduler/index.js";
|
|
29
|
+
import { join } from "node:path";
|
|
30
|
+
import { FleetManagerStateError, FleetManagerConfigError, FleetManagerStateDirError, FleetManagerShutdownError, AgentNotFoundError, ScheduleNotFoundError, ConcurrencyLimitError, InvalidStateError,
|
|
31
|
+
// Job control errors (US-6)
|
|
32
|
+
JobCancelError, JobForkError, JobNotFoundError, } from "./errors.js";
|
|
33
|
+
import { readFleetState } from "../state/fleet-state.js";
|
|
34
|
+
// =============================================================================
|
|
35
|
+
// Constants
|
|
36
|
+
// =============================================================================
|
|
37
|
+
/**
|
|
38
|
+
* Default check interval in milliseconds (1 second)
|
|
39
|
+
*/
|
|
40
|
+
const DEFAULT_CHECK_INTERVAL = 1000;
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// Default Logger
|
|
43
|
+
// =============================================================================
|
|
44
|
+
/**
|
|
45
|
+
* Create a default console-based logger
|
|
46
|
+
*/
|
|
47
|
+
function createDefaultLogger() {
|
|
48
|
+
return {
|
|
49
|
+
debug: (message) => console.debug(`[fleet-manager] ${message}`),
|
|
50
|
+
info: (message) => console.info(`[fleet-manager] ${message}`),
|
|
51
|
+
warn: (message) => console.warn(`[fleet-manager] ${message}`),
|
|
52
|
+
error: (message) => console.error(`[fleet-manager] ${message}`),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// =============================================================================
|
|
56
|
+
// FleetManager Class
|
|
57
|
+
// =============================================================================
|
|
58
|
+
/**
|
|
59
|
+
* FleetManager provides a simple API to manage a fleet of agents
|
|
60
|
+
*
|
|
61
|
+
* This class is the primary entry point for library consumers who want to
|
|
62
|
+
* run herdctl programmatically. It handles:
|
|
63
|
+
*
|
|
64
|
+
* - Configuration loading and validation
|
|
65
|
+
* - State directory initialization
|
|
66
|
+
* - Scheduler lifecycle management
|
|
67
|
+
* - Event emission for monitoring
|
|
68
|
+
*
|
|
69
|
+
* ## Lifecycle
|
|
70
|
+
*
|
|
71
|
+
* 1. **Construction**: Create with options (configPath, stateDir)
|
|
72
|
+
* 2. **Initialize**: Call `initialize()` to load config and prepare state
|
|
73
|
+
* 3. **Start**: Call `start()` to begin scheduler and process schedules
|
|
74
|
+
* 4. **Stop**: Call `stop()` to gracefully shut down
|
|
75
|
+
*
|
|
76
|
+
* ## Events
|
|
77
|
+
*
|
|
78
|
+
* The FleetManager emits events for monitoring:
|
|
79
|
+
* - `initialized` - After successful initialization
|
|
80
|
+
* - `started` - When the scheduler starts running
|
|
81
|
+
* - `stopped` - When the scheduler stops
|
|
82
|
+
* - `error` - When an error occurs
|
|
83
|
+
* - `schedule:trigger` - When a schedule triggers an agent
|
|
84
|
+
* - `schedule:complete` - When an agent run completes
|
|
85
|
+
* - `schedule:error` - When an agent run fails
|
|
86
|
+
*
|
|
87
|
+
* ## Typed Events (US-2)
|
|
88
|
+
*
|
|
89
|
+
* The FleetManager also supports strongly-typed events via TypeScript:
|
|
90
|
+
* - `config:reloaded` - When configuration is hot-reloaded
|
|
91
|
+
* - `agent:started` - When an agent is started
|
|
92
|
+
* - `agent:stopped` - When an agent is stopped
|
|
93
|
+
* - `schedule:triggered` - When a schedule triggers (with payload)
|
|
94
|
+
* - `schedule:skipped` - When a schedule is skipped
|
|
95
|
+
* - `job:created` - When a job is created
|
|
96
|
+
* - `job:output` - When a job produces output
|
|
97
|
+
* - `job:completed` - When a job completes successfully
|
|
98
|
+
* - `job:failed` - When a job fails
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```typescript
|
|
102
|
+
* // Subscribe to typed events
|
|
103
|
+
* manager.on('job:created', (payload) => {
|
|
104
|
+
* console.log(`Job ${payload.job.id} created for ${payload.agentName}`);
|
|
105
|
+
* });
|
|
106
|
+
*
|
|
107
|
+
* manager.on('job:output', (payload) => {
|
|
108
|
+
* process.stdout.write(payload.output);
|
|
109
|
+
* });
|
|
110
|
+
*
|
|
111
|
+
* manager.on('job:completed', (payload) => {
|
|
112
|
+
* console.log(`Job completed in ${payload.durationSeconds}s`);
|
|
113
|
+
* });
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export class FleetManager extends EventEmitter {
|
|
117
|
+
// Configuration
|
|
118
|
+
configPath;
|
|
119
|
+
stateDir;
|
|
120
|
+
logger;
|
|
121
|
+
checkInterval;
|
|
122
|
+
// Internal state
|
|
123
|
+
status = "uninitialized";
|
|
124
|
+
config = null;
|
|
125
|
+
stateDirInfo = null;
|
|
126
|
+
scheduler = null;
|
|
127
|
+
// Timing info
|
|
128
|
+
initializedAt = null;
|
|
129
|
+
startedAt = null;
|
|
130
|
+
stoppedAt = null;
|
|
131
|
+
lastError = null;
|
|
132
|
+
/**
|
|
133
|
+
* Create a new FleetManager instance
|
|
134
|
+
*
|
|
135
|
+
* @param options - Configuration options
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```typescript
|
|
139
|
+
* // Minimal configuration
|
|
140
|
+
* const manager = new FleetManager({
|
|
141
|
+
* configPath: './herdctl.yaml',
|
|
142
|
+
* stateDir: './.herdctl',
|
|
143
|
+
* });
|
|
144
|
+
*
|
|
145
|
+
* // With custom logger
|
|
146
|
+
* const manager = new FleetManager({
|
|
147
|
+
* configPath: './herdctl.yaml',
|
|
148
|
+
* stateDir: './.herdctl',
|
|
149
|
+
* logger: myLogger,
|
|
150
|
+
* checkInterval: 5000, // 5 seconds
|
|
151
|
+
* });
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
constructor(options) {
|
|
155
|
+
super();
|
|
156
|
+
this.configPath = options.configPath;
|
|
157
|
+
this.stateDir = resolve(options.stateDir);
|
|
158
|
+
this.logger = options.logger ?? createDefaultLogger();
|
|
159
|
+
this.checkInterval = options.checkInterval ?? DEFAULT_CHECK_INTERVAL;
|
|
160
|
+
}
|
|
161
|
+
// ===========================================================================
|
|
162
|
+
// Public State Accessors
|
|
163
|
+
// ===========================================================================
|
|
164
|
+
/**
|
|
165
|
+
* Get the current fleet manager state
|
|
166
|
+
*
|
|
167
|
+
* This provides a snapshot of the fleet manager's current status and
|
|
168
|
+
* configuration for monitoring purposes.
|
|
169
|
+
*/
|
|
170
|
+
get state() {
|
|
171
|
+
return {
|
|
172
|
+
status: this.status,
|
|
173
|
+
initializedAt: this.initializedAt,
|
|
174
|
+
startedAt: this.startedAt,
|
|
175
|
+
stoppedAt: this.stoppedAt,
|
|
176
|
+
agentCount: this.config?.agents.length ?? 0,
|
|
177
|
+
lastError: this.lastError,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Get the loaded configuration
|
|
182
|
+
*
|
|
183
|
+
* @returns The resolved configuration, or null if not initialized
|
|
184
|
+
*/
|
|
185
|
+
getConfig() {
|
|
186
|
+
return this.config;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Get the loaded agents
|
|
190
|
+
*
|
|
191
|
+
* @returns Array of resolved agents, or empty array if not initialized
|
|
192
|
+
*/
|
|
193
|
+
getAgents() {
|
|
194
|
+
return this.config?.agents ?? [];
|
|
195
|
+
}
|
|
196
|
+
// ===========================================================================
|
|
197
|
+
// Fleet Status Query Methods (US-3)
|
|
198
|
+
// ===========================================================================
|
|
199
|
+
/**
|
|
200
|
+
* Get overall fleet status
|
|
201
|
+
*
|
|
202
|
+
* Returns a comprehensive snapshot of the fleet state including:
|
|
203
|
+
* - Current state and uptime
|
|
204
|
+
* - Agent counts (total, idle, running, error)
|
|
205
|
+
* - Job counts
|
|
206
|
+
* - Scheduler information
|
|
207
|
+
*
|
|
208
|
+
* This method works whether the fleet is running or stopped.
|
|
209
|
+
*
|
|
210
|
+
* @returns A consistent FleetStatus snapshot
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* ```typescript
|
|
214
|
+
* const status = await manager.getFleetStatus();
|
|
215
|
+
* console.log(`Fleet: ${status.state}`);
|
|
216
|
+
* console.log(`Uptime: ${status.uptimeSeconds}s`);
|
|
217
|
+
* console.log(`Running jobs: ${status.counts.runningJobs}`);
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
async getFleetStatus() {
|
|
221
|
+
// Get agent info to compute counts
|
|
222
|
+
const agentInfoList = await this.getAgentInfo();
|
|
223
|
+
// Compute counts from agent info
|
|
224
|
+
const counts = this.computeFleetCounts(agentInfoList);
|
|
225
|
+
// Compute uptime
|
|
226
|
+
let uptimeSeconds = null;
|
|
227
|
+
if (this.startedAt) {
|
|
228
|
+
const startTime = new Date(this.startedAt).getTime();
|
|
229
|
+
const endTime = this.stoppedAt
|
|
230
|
+
? new Date(this.stoppedAt).getTime()
|
|
231
|
+
: Date.now();
|
|
232
|
+
uptimeSeconds = Math.floor((endTime - startTime) / 1000);
|
|
233
|
+
}
|
|
234
|
+
// Get scheduler state
|
|
235
|
+
const schedulerState = this.scheduler?.getState();
|
|
236
|
+
return {
|
|
237
|
+
state: this.status,
|
|
238
|
+
uptimeSeconds,
|
|
239
|
+
initializedAt: this.initializedAt,
|
|
240
|
+
startedAt: this.startedAt,
|
|
241
|
+
stoppedAt: this.stoppedAt,
|
|
242
|
+
counts,
|
|
243
|
+
scheduler: {
|
|
244
|
+
status: schedulerState?.status ?? "stopped",
|
|
245
|
+
checkCount: schedulerState?.checkCount ?? 0,
|
|
246
|
+
triggerCount: schedulerState?.triggerCount ?? 0,
|
|
247
|
+
lastCheckAt: schedulerState?.lastCheckAt ?? null,
|
|
248
|
+
checkIntervalMs: this.checkInterval,
|
|
249
|
+
},
|
|
250
|
+
lastError: this.lastError,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Get information about all configured agents
|
|
255
|
+
*
|
|
256
|
+
* Returns detailed information for each agent including:
|
|
257
|
+
* - Current status and job information
|
|
258
|
+
* - Schedule details with runtime state
|
|
259
|
+
* - Configuration details
|
|
260
|
+
*
|
|
261
|
+
* This method works whether the fleet is running or stopped.
|
|
262
|
+
*
|
|
263
|
+
* @returns Array of AgentInfo objects with current state
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* ```typescript
|
|
267
|
+
* const agents = await manager.getAgentInfo();
|
|
268
|
+
* for (const agent of agents) {
|
|
269
|
+
* console.log(`${agent.name}: ${agent.status}`);
|
|
270
|
+
* console.log(` Schedules: ${agent.scheduleCount}`);
|
|
271
|
+
* }
|
|
272
|
+
* ```
|
|
273
|
+
*/
|
|
274
|
+
async getAgentInfo() {
|
|
275
|
+
const agents = this.config?.agents ?? [];
|
|
276
|
+
// Read fleet state for runtime information
|
|
277
|
+
const fleetState = await this.readFleetStateSnapshot();
|
|
278
|
+
return agents.map((agent) => {
|
|
279
|
+
const agentState = fleetState.agents[agent.name];
|
|
280
|
+
return this.buildAgentInfo(agent, agentState);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Get information about a specific agent by name
|
|
285
|
+
*
|
|
286
|
+
* Returns detailed information for the specified agent including:
|
|
287
|
+
* - Current status and job information
|
|
288
|
+
* - Schedule details with runtime state
|
|
289
|
+
* - Configuration details
|
|
290
|
+
*
|
|
291
|
+
* This method works whether the fleet is running or stopped.
|
|
292
|
+
*
|
|
293
|
+
* @param name - The agent name to look up
|
|
294
|
+
* @returns AgentInfo for the specified agent
|
|
295
|
+
* @throws {AgentNotFoundError} If no agent with that name exists
|
|
296
|
+
*
|
|
297
|
+
* @example
|
|
298
|
+
* ```typescript
|
|
299
|
+
* const agent = await manager.getAgentInfoByName('my-agent');
|
|
300
|
+
* console.log(`Agent: ${agent.name}`);
|
|
301
|
+
* console.log(`Status: ${agent.status}`);
|
|
302
|
+
* console.log(`Running: ${agent.runningCount}/${agent.maxConcurrent}`);
|
|
303
|
+
* ```
|
|
304
|
+
*/
|
|
305
|
+
async getAgentInfoByName(name) {
|
|
306
|
+
const agents = this.config?.agents ?? [];
|
|
307
|
+
const agent = agents.find((a) => a.name === name);
|
|
308
|
+
if (!agent) {
|
|
309
|
+
throw new AgentNotFoundError(name);
|
|
310
|
+
}
|
|
311
|
+
// Read fleet state for runtime information
|
|
312
|
+
const fleetState = await this.readFleetStateSnapshot();
|
|
313
|
+
const agentState = fleetState.agents[name];
|
|
314
|
+
return this.buildAgentInfo(agent, agentState);
|
|
315
|
+
}
|
|
316
|
+
// ===========================================================================
|
|
317
|
+
// Private Status Query Helpers
|
|
318
|
+
// ===========================================================================
|
|
319
|
+
/**
|
|
320
|
+
* Read fleet state from disk for status queries
|
|
321
|
+
*
|
|
322
|
+
* This provides a consistent snapshot of the fleet state.
|
|
323
|
+
*/
|
|
324
|
+
async readFleetStateSnapshot() {
|
|
325
|
+
if (!this.stateDirInfo) {
|
|
326
|
+
// Not initialized yet, return empty state
|
|
327
|
+
return { fleet: {}, agents: {} };
|
|
328
|
+
}
|
|
329
|
+
return await readFleetState(this.stateDirInfo.stateFile, {
|
|
330
|
+
logger: { warn: this.logger.warn },
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Build AgentInfo from configuration and state
|
|
335
|
+
*/
|
|
336
|
+
buildAgentInfo(agent, agentState) {
|
|
337
|
+
// Build schedule info
|
|
338
|
+
const schedules = this.buildScheduleInfoList(agent, agentState);
|
|
339
|
+
// Get running count from scheduler or state
|
|
340
|
+
const runningCount = this.scheduler?.getRunningJobCount(agent.name) ?? 0;
|
|
341
|
+
// Determine workspace path
|
|
342
|
+
let workspace;
|
|
343
|
+
if (typeof agent.workspace === "string") {
|
|
344
|
+
workspace = agent.workspace;
|
|
345
|
+
}
|
|
346
|
+
else if (agent.workspace?.root) {
|
|
347
|
+
workspace = agent.workspace.root;
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
name: agent.name,
|
|
351
|
+
description: agent.description,
|
|
352
|
+
status: agentState?.status ?? "idle",
|
|
353
|
+
currentJobId: agentState?.current_job ?? null,
|
|
354
|
+
lastJobId: agentState?.last_job ?? null,
|
|
355
|
+
maxConcurrent: agent.instances?.max_concurrent ?? 1,
|
|
356
|
+
runningCount,
|
|
357
|
+
errorMessage: agentState?.error_message ?? null,
|
|
358
|
+
scheduleCount: schedules.length,
|
|
359
|
+
schedules,
|
|
360
|
+
model: agent.model,
|
|
361
|
+
workspace,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Build schedule info list from agent configuration and state
|
|
366
|
+
*/
|
|
367
|
+
buildScheduleInfoList(agent, agentState) {
|
|
368
|
+
if (!agent.schedules) {
|
|
369
|
+
return [];
|
|
370
|
+
}
|
|
371
|
+
return Object.entries(agent.schedules).map(([name, schedule]) => {
|
|
372
|
+
const scheduleState = agentState?.schedules?.[name];
|
|
373
|
+
return {
|
|
374
|
+
name,
|
|
375
|
+
agentName: agent.name,
|
|
376
|
+
type: schedule.type,
|
|
377
|
+
interval: schedule.interval,
|
|
378
|
+
expression: schedule.expression,
|
|
379
|
+
status: scheduleState?.status ?? "idle",
|
|
380
|
+
lastRunAt: scheduleState?.last_run_at ?? null,
|
|
381
|
+
nextRunAt: scheduleState?.next_run_at ?? null,
|
|
382
|
+
lastError: scheduleState?.last_error ?? null,
|
|
383
|
+
};
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Compute fleet counts from agent info list
|
|
388
|
+
*/
|
|
389
|
+
computeFleetCounts(agentInfoList) {
|
|
390
|
+
let idleAgents = 0;
|
|
391
|
+
let runningAgents = 0;
|
|
392
|
+
let errorAgents = 0;
|
|
393
|
+
let totalSchedules = 0;
|
|
394
|
+
let runningSchedules = 0;
|
|
395
|
+
let runningJobs = 0;
|
|
396
|
+
for (const agent of agentInfoList) {
|
|
397
|
+
switch (agent.status) {
|
|
398
|
+
case "idle":
|
|
399
|
+
idleAgents++;
|
|
400
|
+
break;
|
|
401
|
+
case "running":
|
|
402
|
+
runningAgents++;
|
|
403
|
+
break;
|
|
404
|
+
case "error":
|
|
405
|
+
errorAgents++;
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
totalSchedules += agent.scheduleCount;
|
|
409
|
+
runningJobs += agent.runningCount;
|
|
410
|
+
for (const schedule of agent.schedules) {
|
|
411
|
+
if (schedule.status === "running") {
|
|
412
|
+
runningSchedules++;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
totalAgents: agentInfoList.length,
|
|
418
|
+
idleAgents,
|
|
419
|
+
runningAgents,
|
|
420
|
+
errorAgents,
|
|
421
|
+
totalSchedules,
|
|
422
|
+
runningSchedules,
|
|
423
|
+
runningJobs,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
// ===========================================================================
|
|
427
|
+
// Schedule Management Methods (US-7)
|
|
428
|
+
// ===========================================================================
|
|
429
|
+
/**
|
|
430
|
+
* Get all schedules across all agents
|
|
431
|
+
*
|
|
432
|
+
* Returns a list of all configured schedules with their current state,
|
|
433
|
+
* including next trigger times.
|
|
434
|
+
*
|
|
435
|
+
* @returns Array of ScheduleInfo objects with current state
|
|
436
|
+
*
|
|
437
|
+
* @example
|
|
438
|
+
* ```typescript
|
|
439
|
+
* const schedules = await manager.getSchedules();
|
|
440
|
+
* for (const schedule of schedules) {
|
|
441
|
+
* console.log(`${schedule.agentName}/${schedule.name}: ${schedule.status}`);
|
|
442
|
+
* console.log(` Next run: ${schedule.nextRunAt}`);
|
|
443
|
+
* }
|
|
444
|
+
* ```
|
|
445
|
+
*/
|
|
446
|
+
async getSchedules() {
|
|
447
|
+
const agents = this.config?.agents ?? [];
|
|
448
|
+
const fleetState = await this.readFleetStateSnapshot();
|
|
449
|
+
const allSchedules = [];
|
|
450
|
+
for (const agent of agents) {
|
|
451
|
+
const agentState = fleetState.agents[agent.name];
|
|
452
|
+
const schedules = this.buildScheduleInfoList(agent, agentState);
|
|
453
|
+
allSchedules.push(...schedules);
|
|
454
|
+
}
|
|
455
|
+
return allSchedules;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Get a specific schedule by agent name and schedule name
|
|
459
|
+
*
|
|
460
|
+
* @param agentName - The name of the agent
|
|
461
|
+
* @param scheduleName - The name of the schedule
|
|
462
|
+
* @returns The schedule info with current state
|
|
463
|
+
* @throws {AgentNotFoundError} If the agent doesn't exist
|
|
464
|
+
* @throws {ScheduleNotFoundError} If the schedule doesn't exist
|
|
465
|
+
*
|
|
466
|
+
* @example
|
|
467
|
+
* ```typescript
|
|
468
|
+
* const schedule = await manager.getSchedule('my-agent', 'hourly');
|
|
469
|
+
* console.log(`Status: ${schedule.status}`);
|
|
470
|
+
* console.log(`Last run: ${schedule.lastRunAt}`);
|
|
471
|
+
* console.log(`Next run: ${schedule.nextRunAt}`);
|
|
472
|
+
* ```
|
|
473
|
+
*/
|
|
474
|
+
async getSchedule(agentName, scheduleName) {
|
|
475
|
+
const agents = this.config?.agents ?? [];
|
|
476
|
+
const agent = agents.find((a) => a.name === agentName);
|
|
477
|
+
if (!agent) {
|
|
478
|
+
throw new AgentNotFoundError(agentName, {
|
|
479
|
+
availableAgents: agents.map((a) => a.name),
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
if (!agent.schedules || !(scheduleName in agent.schedules)) {
|
|
483
|
+
const availableSchedules = agent.schedules
|
|
484
|
+
? Object.keys(agent.schedules)
|
|
485
|
+
: [];
|
|
486
|
+
throw new ScheduleNotFoundError(agentName, scheduleName, {
|
|
487
|
+
availableSchedules,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
const fleetState = await this.readFleetStateSnapshot();
|
|
491
|
+
const agentState = fleetState.agents[agentName];
|
|
492
|
+
const schedule = agent.schedules[scheduleName];
|
|
493
|
+
const scheduleState = agentState?.schedules?.[scheduleName];
|
|
494
|
+
return {
|
|
495
|
+
name: scheduleName,
|
|
496
|
+
agentName,
|
|
497
|
+
type: schedule.type,
|
|
498
|
+
interval: schedule.interval,
|
|
499
|
+
expression: schedule.expression,
|
|
500
|
+
status: scheduleState?.status ?? "idle",
|
|
501
|
+
lastRunAt: scheduleState?.last_run_at ?? null,
|
|
502
|
+
nextRunAt: scheduleState?.next_run_at ?? null,
|
|
503
|
+
lastError: scheduleState?.last_error ?? null,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Enable a disabled schedule
|
|
508
|
+
*
|
|
509
|
+
* Enables a schedule that was previously disabled, allowing it to trigger
|
|
510
|
+
* again on its configured interval. The enabled state is persisted to the
|
|
511
|
+
* state directory and survives restarts.
|
|
512
|
+
*
|
|
513
|
+
* @param agentName - The name of the agent
|
|
514
|
+
* @param scheduleName - The name of the schedule
|
|
515
|
+
* @returns The updated schedule info
|
|
516
|
+
* @throws {AgentNotFoundError} If the agent doesn't exist
|
|
517
|
+
* @throws {ScheduleNotFoundError} If the schedule doesn't exist
|
|
518
|
+
*
|
|
519
|
+
* @example
|
|
520
|
+
* ```typescript
|
|
521
|
+
* // Enable a previously disabled schedule
|
|
522
|
+
* const schedule = await manager.enableSchedule('my-agent', 'hourly');
|
|
523
|
+
* console.log(`Schedule status: ${schedule.status}`); // 'idle'
|
|
524
|
+
* ```
|
|
525
|
+
*/
|
|
526
|
+
async enableSchedule(agentName, scheduleName) {
|
|
527
|
+
// Validate the agent and schedule exist
|
|
528
|
+
const agents = this.config?.agents ?? [];
|
|
529
|
+
const agent = agents.find((a) => a.name === agentName);
|
|
530
|
+
if (!agent) {
|
|
531
|
+
throw new AgentNotFoundError(agentName, {
|
|
532
|
+
availableAgents: agents.map((a) => a.name),
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
if (!agent.schedules || !(scheduleName in agent.schedules)) {
|
|
536
|
+
const availableSchedules = agent.schedules
|
|
537
|
+
? Object.keys(agent.schedules)
|
|
538
|
+
: [];
|
|
539
|
+
throw new ScheduleNotFoundError(agentName, scheduleName, {
|
|
540
|
+
availableSchedules,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
// Update schedule state to enabled (idle)
|
|
544
|
+
const { updateScheduleState } = await import("../scheduler/schedule-state.js");
|
|
545
|
+
await updateScheduleState(this.stateDir, agentName, scheduleName, { status: "idle" }, { logger: { warn: this.logger.warn } });
|
|
546
|
+
this.logger.info(`Enabled schedule ${agentName}/${scheduleName}`);
|
|
547
|
+
// Return the updated schedule info
|
|
548
|
+
return this.getSchedule(agentName, scheduleName);
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Disable a schedule
|
|
552
|
+
*
|
|
553
|
+
* Disables a schedule, preventing it from triggering on its configured
|
|
554
|
+
* interval. The schedule remains in the configuration but won't run until
|
|
555
|
+
* re-enabled. The disabled state is persisted to the state directory and
|
|
556
|
+
* survives restarts.
|
|
557
|
+
*
|
|
558
|
+
* @param agentName - The name of the agent
|
|
559
|
+
* @param scheduleName - The name of the schedule
|
|
560
|
+
* @returns The updated schedule info
|
|
561
|
+
* @throws {AgentNotFoundError} If the agent doesn't exist
|
|
562
|
+
* @throws {ScheduleNotFoundError} If the schedule doesn't exist
|
|
563
|
+
*
|
|
564
|
+
* @example
|
|
565
|
+
* ```typescript
|
|
566
|
+
* // Disable a schedule temporarily
|
|
567
|
+
* const schedule = await manager.disableSchedule('my-agent', 'hourly');
|
|
568
|
+
* console.log(`Schedule status: ${schedule.status}`); // 'disabled'
|
|
569
|
+
*
|
|
570
|
+
* // Later, re-enable it
|
|
571
|
+
* await manager.enableSchedule('my-agent', 'hourly');
|
|
572
|
+
* ```
|
|
573
|
+
*/
|
|
574
|
+
async disableSchedule(agentName, scheduleName) {
|
|
575
|
+
// Validate the agent and schedule exist
|
|
576
|
+
const agents = this.config?.agents ?? [];
|
|
577
|
+
const agent = agents.find((a) => a.name === agentName);
|
|
578
|
+
if (!agent) {
|
|
579
|
+
throw new AgentNotFoundError(agentName, {
|
|
580
|
+
availableAgents: agents.map((a) => a.name),
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
if (!agent.schedules || !(scheduleName in agent.schedules)) {
|
|
584
|
+
const availableSchedules = agent.schedules
|
|
585
|
+
? Object.keys(agent.schedules)
|
|
586
|
+
: [];
|
|
587
|
+
throw new ScheduleNotFoundError(agentName, scheduleName, {
|
|
588
|
+
availableSchedules,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
// Update schedule state to disabled
|
|
592
|
+
const { updateScheduleState } = await import("../scheduler/schedule-state.js");
|
|
593
|
+
await updateScheduleState(this.stateDir, agentName, scheduleName, { status: "disabled" }, { logger: { warn: this.logger.warn } });
|
|
594
|
+
this.logger.info(`Disabled schedule ${agentName}/${scheduleName}`);
|
|
595
|
+
// Return the updated schedule info
|
|
596
|
+
return this.getSchedule(agentName, scheduleName);
|
|
597
|
+
}
|
|
598
|
+
// ===========================================================================
|
|
599
|
+
// Lifecycle Methods
|
|
600
|
+
// ===========================================================================
|
|
601
|
+
/**
|
|
602
|
+
* Initialize the fleet manager
|
|
603
|
+
*
|
|
604
|
+
* This method:
|
|
605
|
+
* 1. Loads and validates the configuration file
|
|
606
|
+
* 2. Initializes the state directory structure
|
|
607
|
+
* 3. Prepares the scheduler (but does not start it)
|
|
608
|
+
*
|
|
609
|
+
* After initialization, the fleet manager is ready to start.
|
|
610
|
+
*
|
|
611
|
+
* @throws {FleetManagerStateError} If already initialized or running
|
|
612
|
+
* @throws {FleetManagerConfigError} If configuration is invalid or not found
|
|
613
|
+
* @throws {FleetManagerStateDirError} If state directory cannot be created
|
|
614
|
+
*
|
|
615
|
+
* @example
|
|
616
|
+
* ```typescript
|
|
617
|
+
* const manager = new FleetManager({ ... });
|
|
618
|
+
* await manager.initialize();
|
|
619
|
+
* console.log(`Loaded ${manager.state.agentCount} agents`);
|
|
620
|
+
* ```
|
|
621
|
+
*/
|
|
622
|
+
async initialize() {
|
|
623
|
+
// Validate current state
|
|
624
|
+
if (this.status !== "uninitialized" && this.status !== "stopped") {
|
|
625
|
+
throw new FleetManagerStateError("initialize", this.status, ["uninitialized", "stopped"]);
|
|
626
|
+
}
|
|
627
|
+
this.logger.info("Initializing fleet manager...");
|
|
628
|
+
try {
|
|
629
|
+
// Load configuration
|
|
630
|
+
this.logger.debug(this.configPath
|
|
631
|
+
? `Loading config from: ${this.configPath}`
|
|
632
|
+
: "Auto-discovering config...");
|
|
633
|
+
this.config = await this.loadConfiguration();
|
|
634
|
+
this.logger.info(`Loaded ${this.config.agents.length} agent(s) from config`);
|
|
635
|
+
// Initialize state directory
|
|
636
|
+
this.logger.debug(`Initializing state directory: ${this.stateDir}`);
|
|
637
|
+
this.stateDirInfo = await this.initializeStateDir();
|
|
638
|
+
this.logger.debug("State directory initialized");
|
|
639
|
+
// Create scheduler (but don't start it)
|
|
640
|
+
this.scheduler = new Scheduler({
|
|
641
|
+
stateDir: this.stateDir,
|
|
642
|
+
checkInterval: this.checkInterval,
|
|
643
|
+
logger: this.logger,
|
|
644
|
+
onTrigger: (info) => this.handleScheduleTrigger(info),
|
|
645
|
+
});
|
|
646
|
+
// Update state
|
|
647
|
+
this.status = "initialized";
|
|
648
|
+
this.initializedAt = new Date().toISOString();
|
|
649
|
+
this.lastError = null;
|
|
650
|
+
this.logger.info("Fleet manager initialized successfully");
|
|
651
|
+
this.emit("initialized");
|
|
652
|
+
}
|
|
653
|
+
catch (error) {
|
|
654
|
+
this.status = "error";
|
|
655
|
+
this.lastError = error instanceof Error ? error.message : String(error);
|
|
656
|
+
this.emit("error", error instanceof Error ? error : new Error(String(error)));
|
|
657
|
+
throw error;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Start the fleet manager
|
|
662
|
+
*
|
|
663
|
+
* This begins the scheduler, which will:
|
|
664
|
+
* 1. Check agent schedules at the configured interval
|
|
665
|
+
* 2. Trigger agents when their schedules are due
|
|
666
|
+
* 3. Track schedule state in the state directory
|
|
667
|
+
*
|
|
668
|
+
* @throws {FleetManagerStateError} If not initialized
|
|
669
|
+
*
|
|
670
|
+
* @example
|
|
671
|
+
* ```typescript
|
|
672
|
+
* await manager.initialize();
|
|
673
|
+
* await manager.start();
|
|
674
|
+
*
|
|
675
|
+
* // The manager is now running and processing schedules
|
|
676
|
+
* manager.on('schedule:trigger', (agent, schedule) => {
|
|
677
|
+
* console.log(`Triggered ${agent}/${schedule}`);
|
|
678
|
+
* });
|
|
679
|
+
* ```
|
|
680
|
+
*/
|
|
681
|
+
async start() {
|
|
682
|
+
// Validate current state
|
|
683
|
+
if (this.status !== "initialized") {
|
|
684
|
+
throw new FleetManagerStateError("start", this.status, "initialized");
|
|
685
|
+
}
|
|
686
|
+
this.logger.info("Starting fleet manager...");
|
|
687
|
+
this.status = "starting";
|
|
688
|
+
try {
|
|
689
|
+
// Start the scheduler with loaded agents
|
|
690
|
+
const agents = this.config.agents;
|
|
691
|
+
// Start scheduler in background (don't await the loop)
|
|
692
|
+
this.startSchedulerAsync(agents);
|
|
693
|
+
// Update state
|
|
694
|
+
this.status = "running";
|
|
695
|
+
this.startedAt = new Date().toISOString();
|
|
696
|
+
this.stoppedAt = null;
|
|
697
|
+
this.logger.info("Fleet manager started");
|
|
698
|
+
this.emit("started");
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
this.status = "error";
|
|
702
|
+
this.lastError = error instanceof Error ? error.message : String(error);
|
|
703
|
+
this.emit("error", error instanceof Error ? error : new Error(String(error)));
|
|
704
|
+
throw error;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Stop the fleet manager gracefully
|
|
709
|
+
*
|
|
710
|
+
* This will:
|
|
711
|
+
* 1. Signal the scheduler to stop accepting new triggers
|
|
712
|
+
* 2. Wait for running jobs to complete (with timeout)
|
|
713
|
+
* 3. If timeout is reached and cancelOnTimeout is true, cancel remaining jobs
|
|
714
|
+
* 4. Persist all state before shutdown completes
|
|
715
|
+
* 5. Emit 'stopped' event when complete
|
|
716
|
+
*
|
|
717
|
+
* @param options - Stop options for controlling shutdown behavior
|
|
718
|
+
* @throws {FleetManagerShutdownError} If shutdown times out and cancelOnTimeout is false
|
|
719
|
+
*
|
|
720
|
+
* @example
|
|
721
|
+
* ```typescript
|
|
722
|
+
* // Normal shutdown - wait for jobs with default 30s timeout
|
|
723
|
+
* await manager.stop();
|
|
724
|
+
*
|
|
725
|
+
* // Shutdown with custom timeout
|
|
726
|
+
* await manager.stop({ timeout: 60000 });
|
|
727
|
+
*
|
|
728
|
+
* // Shutdown without waiting for jobs (not recommended)
|
|
729
|
+
* await manager.stop({ waitForJobs: false });
|
|
730
|
+
*
|
|
731
|
+
* // Cancel jobs if they don't complete in time
|
|
732
|
+
* await manager.stop({
|
|
733
|
+
* timeout: 30000,
|
|
734
|
+
* cancelOnTimeout: true,
|
|
735
|
+
* cancelTimeout: 10000,
|
|
736
|
+
* });
|
|
737
|
+
* ```
|
|
738
|
+
*/
|
|
739
|
+
async stop(options) {
|
|
740
|
+
if (this.status !== "running" && this.status !== "starting") {
|
|
741
|
+
this.logger.debug(`Stop called but status is '${this.status}', ignoring`);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
const waitForJobs = options?.waitForJobs ?? true;
|
|
745
|
+
const timeout = options?.timeout ?? 30000;
|
|
746
|
+
const cancelOnTimeout = options?.cancelOnTimeout ?? false;
|
|
747
|
+
const cancelTimeout = options?.cancelTimeout ?? 10000;
|
|
748
|
+
this.logger.info("Stopping fleet manager...");
|
|
749
|
+
this.status = "stopping";
|
|
750
|
+
try {
|
|
751
|
+
// Stop the scheduler - don't wait for jobs here, we'll handle it ourselves
|
|
752
|
+
// This stops new triggers from being accepted
|
|
753
|
+
if (this.scheduler) {
|
|
754
|
+
try {
|
|
755
|
+
await this.scheduler.stop({
|
|
756
|
+
waitForJobs,
|
|
757
|
+
timeout,
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
catch (error) {
|
|
761
|
+
// Check if it's a scheduler shutdown timeout
|
|
762
|
+
if (error instanceof Error && error.name === "SchedulerShutdownError") {
|
|
763
|
+
if (cancelOnTimeout) {
|
|
764
|
+
// Cancel all running jobs
|
|
765
|
+
this.logger.info("Timeout reached, cancelling running jobs...");
|
|
766
|
+
await this.cancelRunningJobs(cancelTimeout);
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
// Re-throw the error
|
|
770
|
+
this.status = "error";
|
|
771
|
+
this.lastError = error.message;
|
|
772
|
+
throw new FleetManagerShutdownError(error.message, {
|
|
773
|
+
timedOut: true,
|
|
774
|
+
cause: error,
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
else {
|
|
779
|
+
throw error;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
// Persist fleet state before completing shutdown
|
|
784
|
+
await this.persistShutdownState();
|
|
785
|
+
// Update state
|
|
786
|
+
this.status = "stopped";
|
|
787
|
+
this.stoppedAt = new Date().toISOString();
|
|
788
|
+
this.logger.info("Fleet manager stopped");
|
|
789
|
+
this.emit("stopped");
|
|
790
|
+
}
|
|
791
|
+
catch (error) {
|
|
792
|
+
this.status = "error";
|
|
793
|
+
this.lastError = error instanceof Error ? error.message : String(error);
|
|
794
|
+
throw error;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Reload configuration without restarting the fleet
|
|
799
|
+
*
|
|
800
|
+
* This method provides hot configuration reload capability:
|
|
801
|
+
* 1. Loads and validates the new configuration
|
|
802
|
+
* 2. If validation fails, keeps the old configuration (fails gracefully)
|
|
803
|
+
* 3. Running jobs continue with their original configuration
|
|
804
|
+
* 4. New jobs will use the new configuration
|
|
805
|
+
* 5. Updates the scheduler with new agent definitions and schedules
|
|
806
|
+
* 6. Emits a 'config:reloaded' event with a list of changes
|
|
807
|
+
*
|
|
808
|
+
* @returns The reload result with change details
|
|
809
|
+
* @throws {InvalidStateError} If the fleet manager is not initialized
|
|
810
|
+
* @throws {FleetManagerConfigError} If the new configuration is invalid (re-thrown after logging)
|
|
811
|
+
*
|
|
812
|
+
* @example
|
|
813
|
+
* ```typescript
|
|
814
|
+
* // Reload configuration
|
|
815
|
+
* const result = await manager.reload();
|
|
816
|
+
* console.log(`Reloaded with ${result.changes.length} changes`);
|
|
817
|
+
*
|
|
818
|
+
* // Subscribe to reload events
|
|
819
|
+
* manager.on('config:reloaded', (payload) => {
|
|
820
|
+
* console.log(`Config reloaded: ${payload.changes.length} changes`);
|
|
821
|
+
* for (const change of payload.changes) {
|
|
822
|
+
* console.log(` ${change.type} ${change.category}: ${change.name}`);
|
|
823
|
+
* }
|
|
824
|
+
* });
|
|
825
|
+
* ```
|
|
826
|
+
*/
|
|
827
|
+
async reload() {
|
|
828
|
+
// Validate state - must be at least initialized
|
|
829
|
+
if (this.status === "uninitialized") {
|
|
830
|
+
throw new InvalidStateError("reload", this.status, ["initialized", "starting", "running", "stopping", "stopped"]);
|
|
831
|
+
}
|
|
832
|
+
this.logger.info("Reloading configuration...");
|
|
833
|
+
// Store old config for comparison
|
|
834
|
+
const oldConfig = this.config;
|
|
835
|
+
// Try to load new configuration
|
|
836
|
+
let newConfig;
|
|
837
|
+
try {
|
|
838
|
+
newConfig = await this.loadConfiguration();
|
|
839
|
+
}
|
|
840
|
+
catch (error) {
|
|
841
|
+
// Log the error but don't update config - fail gracefully
|
|
842
|
+
this.logger.error(`Failed to reload configuration: ${error instanceof Error ? error.message : String(error)}`);
|
|
843
|
+
this.logger.info("Keeping existing configuration");
|
|
844
|
+
// Re-throw so caller knows reload failed
|
|
845
|
+
throw error;
|
|
846
|
+
}
|
|
847
|
+
// Compute changes between old and new config
|
|
848
|
+
const changes = this.computeConfigChanges(oldConfig, newConfig);
|
|
849
|
+
// Update the stored configuration
|
|
850
|
+
this.config = newConfig;
|
|
851
|
+
// Update the scheduler with new agents (if scheduler exists and is running)
|
|
852
|
+
if (this.scheduler) {
|
|
853
|
+
this.scheduler.setAgents(newConfig.agents);
|
|
854
|
+
this.logger.debug(`Updated scheduler with ${newConfig.agents.length} agents`);
|
|
855
|
+
}
|
|
856
|
+
const timestamp = new Date().toISOString();
|
|
857
|
+
// Build the reload payload
|
|
858
|
+
const payload = {
|
|
859
|
+
agentCount: newConfig.agents.length,
|
|
860
|
+
agentNames: newConfig.agents.map((a) => a.name),
|
|
861
|
+
configPath: newConfig.configPath,
|
|
862
|
+
changes,
|
|
863
|
+
timestamp,
|
|
864
|
+
};
|
|
865
|
+
// Emit the config:reloaded event
|
|
866
|
+
this.emit("config:reloaded", payload);
|
|
867
|
+
this.logger.info(`Configuration reloaded: ${newConfig.agents.length} agents, ${changes.length} changes`);
|
|
868
|
+
return payload;
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Compute the list of changes between old and new configuration
|
|
872
|
+
*/
|
|
873
|
+
computeConfigChanges(oldConfig, newConfig) {
|
|
874
|
+
const changes = [];
|
|
875
|
+
const oldAgents = oldConfig?.agents ?? [];
|
|
876
|
+
const newAgents = newConfig.agents;
|
|
877
|
+
const oldAgentNames = new Set(oldAgents.map((a) => a.name));
|
|
878
|
+
const newAgentNames = new Set(newAgents.map((a) => a.name));
|
|
879
|
+
// Find added agents
|
|
880
|
+
for (const agent of newAgents) {
|
|
881
|
+
if (!oldAgentNames.has(agent.name)) {
|
|
882
|
+
changes.push({
|
|
883
|
+
type: "added",
|
|
884
|
+
category: "agent",
|
|
885
|
+
name: agent.name,
|
|
886
|
+
details: agent.description,
|
|
887
|
+
});
|
|
888
|
+
// Also add all schedules for new agents
|
|
889
|
+
if (agent.schedules) {
|
|
890
|
+
for (const scheduleName of Object.keys(agent.schedules)) {
|
|
891
|
+
changes.push({
|
|
892
|
+
type: "added",
|
|
893
|
+
category: "schedule",
|
|
894
|
+
name: `${agent.name}/${scheduleName}`,
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
// Find removed agents
|
|
901
|
+
for (const agent of oldAgents) {
|
|
902
|
+
if (!newAgentNames.has(agent.name)) {
|
|
903
|
+
changes.push({
|
|
904
|
+
type: "removed",
|
|
905
|
+
category: "agent",
|
|
906
|
+
name: agent.name,
|
|
907
|
+
});
|
|
908
|
+
// Also mark all schedules as removed
|
|
909
|
+
if (agent.schedules) {
|
|
910
|
+
for (const scheduleName of Object.keys(agent.schedules)) {
|
|
911
|
+
changes.push({
|
|
912
|
+
type: "removed",
|
|
913
|
+
category: "schedule",
|
|
914
|
+
name: `${agent.name}/${scheduleName}`,
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
// Find modified agents and schedules
|
|
921
|
+
for (const newAgent of newAgents) {
|
|
922
|
+
const oldAgent = oldAgents.find((a) => a.name === newAgent.name);
|
|
923
|
+
if (!oldAgent) {
|
|
924
|
+
continue; // Already handled as "added"
|
|
925
|
+
}
|
|
926
|
+
// Check for agent-level modifications
|
|
927
|
+
const agentModified = this.isAgentModified(oldAgent, newAgent);
|
|
928
|
+
if (agentModified) {
|
|
929
|
+
changes.push({
|
|
930
|
+
type: "modified",
|
|
931
|
+
category: "agent",
|
|
932
|
+
name: newAgent.name,
|
|
933
|
+
details: agentModified,
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
// Check for schedule changes
|
|
937
|
+
const oldScheduleNames = new Set(oldAgent.schedules ? Object.keys(oldAgent.schedules) : []);
|
|
938
|
+
const newScheduleNames = new Set(newAgent.schedules ? Object.keys(newAgent.schedules) : []);
|
|
939
|
+
// Added schedules
|
|
940
|
+
for (const scheduleName of newScheduleNames) {
|
|
941
|
+
if (!oldScheduleNames.has(scheduleName)) {
|
|
942
|
+
changes.push({
|
|
943
|
+
type: "added",
|
|
944
|
+
category: "schedule",
|
|
945
|
+
name: `${newAgent.name}/${scheduleName}`,
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
// Removed schedules
|
|
950
|
+
for (const scheduleName of oldScheduleNames) {
|
|
951
|
+
if (!newScheduleNames.has(scheduleName)) {
|
|
952
|
+
changes.push({
|
|
953
|
+
type: "removed",
|
|
954
|
+
category: "schedule",
|
|
955
|
+
name: `${newAgent.name}/${scheduleName}`,
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
// Modified schedules
|
|
960
|
+
for (const scheduleName of newScheduleNames) {
|
|
961
|
+
if (oldScheduleNames.has(scheduleName)) {
|
|
962
|
+
const oldSchedule = oldAgent.schedules[scheduleName];
|
|
963
|
+
const newSchedule = newAgent.schedules[scheduleName];
|
|
964
|
+
if (this.isScheduleModified(oldSchedule, newSchedule)) {
|
|
965
|
+
changes.push({
|
|
966
|
+
type: "modified",
|
|
967
|
+
category: "schedule",
|
|
968
|
+
name: `${newAgent.name}/${scheduleName}`,
|
|
969
|
+
details: this.getScheduleModificationDetails(oldSchedule, newSchedule),
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return changes;
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Check if an agent configuration has been modified
|
|
979
|
+
* Returns a description of what changed, or null if not modified
|
|
980
|
+
*/
|
|
981
|
+
isAgentModified(oldAgent, newAgent) {
|
|
982
|
+
const modifications = [];
|
|
983
|
+
// Check key properties
|
|
984
|
+
if (oldAgent.description !== newAgent.description) {
|
|
985
|
+
modifications.push("description");
|
|
986
|
+
}
|
|
987
|
+
if (oldAgent.model !== newAgent.model) {
|
|
988
|
+
modifications.push("model");
|
|
989
|
+
}
|
|
990
|
+
if (oldAgent.max_turns !== newAgent.max_turns) {
|
|
991
|
+
modifications.push("max_turns");
|
|
992
|
+
}
|
|
993
|
+
if (oldAgent.system_prompt !== newAgent.system_prompt) {
|
|
994
|
+
modifications.push("system_prompt");
|
|
995
|
+
}
|
|
996
|
+
// Check workspace
|
|
997
|
+
const oldWorkspace = typeof oldAgent.workspace === "string"
|
|
998
|
+
? oldAgent.workspace
|
|
999
|
+
: oldAgent.workspace?.root;
|
|
1000
|
+
const newWorkspace = typeof newAgent.workspace === "string"
|
|
1001
|
+
? newAgent.workspace
|
|
1002
|
+
: newAgent.workspace?.root;
|
|
1003
|
+
if (oldWorkspace !== newWorkspace) {
|
|
1004
|
+
modifications.push("workspace");
|
|
1005
|
+
}
|
|
1006
|
+
// Check instances
|
|
1007
|
+
const oldMaxConcurrent = oldAgent.instances?.max_concurrent ?? 1;
|
|
1008
|
+
const newMaxConcurrent = newAgent.instances?.max_concurrent ?? 1;
|
|
1009
|
+
if (oldMaxConcurrent !== newMaxConcurrent) {
|
|
1010
|
+
modifications.push("max_concurrent");
|
|
1011
|
+
}
|
|
1012
|
+
return modifications.length > 0 ? modifications.join(", ") : null;
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Check if a schedule configuration has been modified
|
|
1016
|
+
*/
|
|
1017
|
+
isScheduleModified(oldSchedule, newSchedule) {
|
|
1018
|
+
return (oldSchedule.type !== newSchedule.type ||
|
|
1019
|
+
oldSchedule.interval !== newSchedule.interval ||
|
|
1020
|
+
oldSchedule.expression !== newSchedule.expression ||
|
|
1021
|
+
oldSchedule.prompt !== newSchedule.prompt);
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Get a description of what changed in a schedule
|
|
1025
|
+
*/
|
|
1026
|
+
getScheduleModificationDetails(oldSchedule, newSchedule) {
|
|
1027
|
+
const details = [];
|
|
1028
|
+
if (oldSchedule.type !== newSchedule.type) {
|
|
1029
|
+
details.push(`type: ${oldSchedule.type} → ${newSchedule.type}`);
|
|
1030
|
+
}
|
|
1031
|
+
if (oldSchedule.interval !== newSchedule.interval) {
|
|
1032
|
+
details.push(`interval: ${oldSchedule.interval ?? "none"} → ${newSchedule.interval ?? "none"}`);
|
|
1033
|
+
}
|
|
1034
|
+
if (oldSchedule.expression !== newSchedule.expression) {
|
|
1035
|
+
details.push(`expression: ${oldSchedule.expression ?? "none"} → ${newSchedule.expression ?? "none"}`);
|
|
1036
|
+
}
|
|
1037
|
+
if (oldSchedule.prompt !== newSchedule.prompt) {
|
|
1038
|
+
details.push("prompt changed");
|
|
1039
|
+
}
|
|
1040
|
+
return details.join("; ");
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Cancel all running jobs during shutdown
|
|
1044
|
+
*
|
|
1045
|
+
* @param cancelTimeout - Timeout for each job cancellation
|
|
1046
|
+
*/
|
|
1047
|
+
async cancelRunningJobs(cancelTimeout) {
|
|
1048
|
+
const jobsDir = join(this.stateDir, "jobs");
|
|
1049
|
+
// Get all running jobs from the fleet status
|
|
1050
|
+
const agentInfoList = await this.getAgentInfo();
|
|
1051
|
+
const runningJobIds = [];
|
|
1052
|
+
for (const agent of agentInfoList) {
|
|
1053
|
+
if (agent.currentJobId) {
|
|
1054
|
+
runningJobIds.push(agent.currentJobId);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (runningJobIds.length === 0) {
|
|
1058
|
+
this.logger.debug("No running jobs to cancel");
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
this.logger.info(`Cancelling ${runningJobIds.length} running job(s)...`);
|
|
1062
|
+
// Cancel all jobs in parallel
|
|
1063
|
+
const cancelPromises = runningJobIds.map(async (jobId) => {
|
|
1064
|
+
try {
|
|
1065
|
+
const result = await this.cancelJob(jobId, { timeout: cancelTimeout });
|
|
1066
|
+
this.logger.debug(`Cancelled job ${jobId}: ${result.terminationType}`);
|
|
1067
|
+
}
|
|
1068
|
+
catch (error) {
|
|
1069
|
+
this.logger.warn(`Failed to cancel job ${jobId}: ${error.message}`);
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
await Promise.all(cancelPromises);
|
|
1073
|
+
this.logger.info("All jobs cancelled");
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Persist shutdown state to ensure all state is saved before completing
|
|
1077
|
+
*/
|
|
1078
|
+
async persistShutdownState() {
|
|
1079
|
+
if (!this.stateDirInfo) {
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
// Persist fleet state
|
|
1083
|
+
const { writeFleetState } = await import("../state/fleet-state.js");
|
|
1084
|
+
// Read current state and update with stopped status
|
|
1085
|
+
const currentState = await this.readFleetStateSnapshot();
|
|
1086
|
+
// Update fleet-level state
|
|
1087
|
+
const updatedState = {
|
|
1088
|
+
...currentState,
|
|
1089
|
+
fleet: {
|
|
1090
|
+
...currentState.fleet,
|
|
1091
|
+
stoppedAt: new Date().toISOString(),
|
|
1092
|
+
},
|
|
1093
|
+
};
|
|
1094
|
+
try {
|
|
1095
|
+
await writeFleetState(this.stateDirInfo.stateFile, updatedState);
|
|
1096
|
+
this.logger.debug("Fleet state persisted");
|
|
1097
|
+
}
|
|
1098
|
+
catch (error) {
|
|
1099
|
+
this.logger.warn(`Failed to persist fleet state: ${error.message}`);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
// ===========================================================================
|
|
1103
|
+
// Manual Triggering (US-5)
|
|
1104
|
+
// ===========================================================================
|
|
1105
|
+
/**
|
|
1106
|
+
* Manually trigger an agent outside its normal schedule
|
|
1107
|
+
*
|
|
1108
|
+
* This method allows you to trigger an agent on-demand for testing or
|
|
1109
|
+
* handling urgent situations. You can optionally specify a schedule to use
|
|
1110
|
+
* for configuration (prompt, work source, etc.) or pass runtime options
|
|
1111
|
+
* to override defaults.
|
|
1112
|
+
*
|
|
1113
|
+
* @param agentName - Name of the agent to trigger
|
|
1114
|
+
* @param scheduleName - Optional schedule name to use for configuration
|
|
1115
|
+
* @param options - Optional runtime options to override defaults
|
|
1116
|
+
* @returns The created job information
|
|
1117
|
+
* @throws {InvalidStateError} If the fleet manager is not initialized
|
|
1118
|
+
* @throws {AgentNotFoundError} If the agent doesn't exist
|
|
1119
|
+
* @throws {ScheduleNotFoundError} If the specified schedule doesn't exist
|
|
1120
|
+
* @throws {ConcurrencyLimitError} If the agent is at capacity and bypassConcurrencyLimit is false
|
|
1121
|
+
*
|
|
1122
|
+
* @example
|
|
1123
|
+
* ```typescript
|
|
1124
|
+
* // Trigger with agent defaults
|
|
1125
|
+
* const job = await manager.trigger('my-agent');
|
|
1126
|
+
*
|
|
1127
|
+
* // Trigger a specific schedule
|
|
1128
|
+
* const job = await manager.trigger('my-agent', 'hourly');
|
|
1129
|
+
*
|
|
1130
|
+
* // Trigger with custom prompt
|
|
1131
|
+
* const job = await manager.trigger('my-agent', undefined, {
|
|
1132
|
+
* prompt: 'Review the latest security updates',
|
|
1133
|
+
* });
|
|
1134
|
+
*
|
|
1135
|
+
* // Force trigger even at capacity
|
|
1136
|
+
* const job = await manager.trigger('my-agent', undefined, {
|
|
1137
|
+
* bypassConcurrencyLimit: true,
|
|
1138
|
+
* });
|
|
1139
|
+
* ```
|
|
1140
|
+
*/
|
|
1141
|
+
async trigger(agentName, scheduleName, options) {
|
|
1142
|
+
// Validate state - must be at least initialized
|
|
1143
|
+
if (this.status === "uninitialized") {
|
|
1144
|
+
throw new InvalidStateError("trigger", this.status, ["initialized", "running", "stopped"]);
|
|
1145
|
+
}
|
|
1146
|
+
// Find the agent
|
|
1147
|
+
const agents = this.config?.agents ?? [];
|
|
1148
|
+
const agent = agents.find((a) => a.name === agentName);
|
|
1149
|
+
if (!agent) {
|
|
1150
|
+
throw new AgentNotFoundError(agentName, {
|
|
1151
|
+
availableAgents: agents.map((a) => a.name),
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
// If a schedule name is provided, validate it exists
|
|
1155
|
+
let schedule;
|
|
1156
|
+
if (scheduleName) {
|
|
1157
|
+
if (!agent.schedules || !(scheduleName in agent.schedules)) {
|
|
1158
|
+
const availableSchedules = agent.schedules
|
|
1159
|
+
? Object.keys(agent.schedules)
|
|
1160
|
+
: [];
|
|
1161
|
+
throw new ScheduleNotFoundError(agentName, scheduleName, {
|
|
1162
|
+
availableSchedules,
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
schedule = agent.schedules[scheduleName];
|
|
1166
|
+
}
|
|
1167
|
+
// Check concurrency limits unless bypassed
|
|
1168
|
+
if (!options?.bypassConcurrencyLimit) {
|
|
1169
|
+
const maxConcurrent = agent.instances?.max_concurrent ?? 1;
|
|
1170
|
+
const runningCount = this.scheduler?.getRunningJobCount(agentName) ?? 0;
|
|
1171
|
+
if (runningCount >= maxConcurrent) {
|
|
1172
|
+
throw new ConcurrencyLimitError(agentName, runningCount, maxConcurrent);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
// Determine the prompt to use (priority: options > schedule > agent default)
|
|
1176
|
+
const prompt = options?.prompt ?? schedule?.prompt ?? undefined;
|
|
1177
|
+
// Create the job
|
|
1178
|
+
const jobsDir = join(this.stateDir, "jobs");
|
|
1179
|
+
const job = await createJob(jobsDir, {
|
|
1180
|
+
agent: agentName,
|
|
1181
|
+
trigger_type: "manual",
|
|
1182
|
+
schedule: scheduleName ?? null,
|
|
1183
|
+
prompt: prompt ?? null,
|
|
1184
|
+
});
|
|
1185
|
+
const timestamp = new Date().toISOString();
|
|
1186
|
+
this.logger.info(`Manually triggered ${agentName}${scheduleName ? `/${scheduleName}` : ""} - job ${job.id}`);
|
|
1187
|
+
// Emit job:created event
|
|
1188
|
+
this.emit("job:created", {
|
|
1189
|
+
job,
|
|
1190
|
+
agentName,
|
|
1191
|
+
scheduleName: scheduleName ?? null,
|
|
1192
|
+
timestamp,
|
|
1193
|
+
});
|
|
1194
|
+
// Build and return the result
|
|
1195
|
+
const result = {
|
|
1196
|
+
jobId: job.id,
|
|
1197
|
+
agentName,
|
|
1198
|
+
scheduleName: scheduleName ?? null,
|
|
1199
|
+
startedAt: job.started_at,
|
|
1200
|
+
prompt,
|
|
1201
|
+
};
|
|
1202
|
+
return result;
|
|
1203
|
+
}
|
|
1204
|
+
// ===========================================================================
|
|
1205
|
+
// Job Control (US-6)
|
|
1206
|
+
// ===========================================================================
|
|
1207
|
+
/**
|
|
1208
|
+
* Cancel a running job gracefully
|
|
1209
|
+
*
|
|
1210
|
+
* This method cancels a running job by first sending SIGTERM to allow
|
|
1211
|
+
* graceful shutdown. If the job doesn't terminate within the timeout,
|
|
1212
|
+
* it will be forcefully killed with SIGKILL.
|
|
1213
|
+
*
|
|
1214
|
+
* @param jobId - ID of the job to cancel
|
|
1215
|
+
* @param options - Optional cancellation options
|
|
1216
|
+
* @param options.timeout - Time in ms to wait for graceful shutdown (default: 10000)
|
|
1217
|
+
* @returns Result of the cancellation operation
|
|
1218
|
+
* @throws {InvalidStateError} If the fleet manager is not initialized
|
|
1219
|
+
* @throws {JobNotFoundError} If the job doesn't exist
|
|
1220
|
+
*
|
|
1221
|
+
* @example
|
|
1222
|
+
* ```typescript
|
|
1223
|
+
* // Cancel with default timeout
|
|
1224
|
+
* const result = await manager.cancelJob('job-2024-01-15-abc123');
|
|
1225
|
+
* console.log(`Job cancelled: ${result.terminationType}`);
|
|
1226
|
+
*
|
|
1227
|
+
* // Cancel with custom timeout
|
|
1228
|
+
* const result = await manager.cancelJob('job-2024-01-15-abc123', {
|
|
1229
|
+
* timeout: 30000, // 30 seconds
|
|
1230
|
+
* });
|
|
1231
|
+
* ```
|
|
1232
|
+
*/
|
|
1233
|
+
async cancelJob(jobId, options) {
|
|
1234
|
+
// Validate state - must be at least initialized
|
|
1235
|
+
if (this.status === "uninitialized") {
|
|
1236
|
+
throw new InvalidStateError("cancelJob", this.status, ["initialized", "running", "stopped"]);
|
|
1237
|
+
}
|
|
1238
|
+
const jobsDir = join(this.stateDir, "jobs");
|
|
1239
|
+
const timeout = options?.timeout ?? 10000; // Default 10 seconds
|
|
1240
|
+
// Get the job to verify it exists and check its status
|
|
1241
|
+
const job = await getJob(jobsDir, jobId, { logger: this.logger });
|
|
1242
|
+
if (!job) {
|
|
1243
|
+
throw new JobNotFoundError(jobId);
|
|
1244
|
+
}
|
|
1245
|
+
const timestamp = new Date().toISOString();
|
|
1246
|
+
let terminationType;
|
|
1247
|
+
let durationSeconds;
|
|
1248
|
+
// If job is already not running, return early
|
|
1249
|
+
if (job.status !== "running" && job.status !== "pending") {
|
|
1250
|
+
this.logger.info(`Job ${jobId} is already ${job.status}, no cancellation needed`);
|
|
1251
|
+
terminationType = 'already_stopped';
|
|
1252
|
+
// Calculate duration if we have finished_at
|
|
1253
|
+
if (job.finished_at) {
|
|
1254
|
+
const startTime = new Date(job.started_at).getTime();
|
|
1255
|
+
const endTime = new Date(job.finished_at).getTime();
|
|
1256
|
+
durationSeconds = Math.round((endTime - startTime) / 1000);
|
|
1257
|
+
}
|
|
1258
|
+
return {
|
|
1259
|
+
jobId,
|
|
1260
|
+
success: true,
|
|
1261
|
+
terminationType,
|
|
1262
|
+
canceledAt: timestamp,
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
// Calculate duration
|
|
1266
|
+
const startTime = new Date(job.started_at).getTime();
|
|
1267
|
+
const endTime = new Date(timestamp).getTime();
|
|
1268
|
+
durationSeconds = Math.round((endTime - startTime) / 1000);
|
|
1269
|
+
// Try to cancel via the scheduler if it has process tracking
|
|
1270
|
+
// For now, we'll update the job status directly since we don't have
|
|
1271
|
+
// direct process control yet. In a full implementation, this would
|
|
1272
|
+
// send SIGTERM to the process, wait, then SIGKILL if needed.
|
|
1273
|
+
// Note: The scheduler/executor would need to track job processes
|
|
1274
|
+
// and provide an API to cancel them. For this implementation,
|
|
1275
|
+
// we'll update the job state and emit events, assuming the executor
|
|
1276
|
+
// monitors the job status and handles cancellation.
|
|
1277
|
+
this.logger.info(`Cancelling job ${jobId} for agent ${job.agent}`);
|
|
1278
|
+
// Update job status to cancelled
|
|
1279
|
+
try {
|
|
1280
|
+
await updateJob(jobsDir, jobId, {
|
|
1281
|
+
status: "cancelled",
|
|
1282
|
+
exit_reason: "cancelled",
|
|
1283
|
+
finished_at: timestamp,
|
|
1284
|
+
});
|
|
1285
|
+
// Assume graceful termination for now
|
|
1286
|
+
// In a full implementation, this would be determined by whether
|
|
1287
|
+
// the process responded to SIGTERM or required SIGKILL
|
|
1288
|
+
terminationType = 'graceful';
|
|
1289
|
+
}
|
|
1290
|
+
catch (error) {
|
|
1291
|
+
this.logger.error(`Failed to update job status: ${error.message}`);
|
|
1292
|
+
throw new JobCancelError(jobId, 'process_error', {
|
|
1293
|
+
cause: error,
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
// Emit job:cancelled event
|
|
1297
|
+
const updatedJob = await getJob(jobsDir, jobId, { logger: this.logger });
|
|
1298
|
+
if (updatedJob) {
|
|
1299
|
+
this.emit("job:cancelled", {
|
|
1300
|
+
job: updatedJob,
|
|
1301
|
+
agentName: job.agent,
|
|
1302
|
+
terminationType,
|
|
1303
|
+
durationSeconds,
|
|
1304
|
+
timestamp,
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
this.logger.info(`Job ${jobId} cancelled (${terminationType}) after ${durationSeconds}s`);
|
|
1308
|
+
return {
|
|
1309
|
+
jobId,
|
|
1310
|
+
success: true,
|
|
1311
|
+
terminationType,
|
|
1312
|
+
canceledAt: timestamp,
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Fork a job to create a new job based on an existing one
|
|
1317
|
+
*
|
|
1318
|
+
* This method creates a new job that is based on an existing job's
|
|
1319
|
+
* configuration. The new job will have the same agent and can optionally
|
|
1320
|
+
* have modifications applied (different prompt, schedule, etc.).
|
|
1321
|
+
*
|
|
1322
|
+
* If the original job has a session ID, the new job will fork from that
|
|
1323
|
+
* session, preserving conversation context.
|
|
1324
|
+
*
|
|
1325
|
+
* @param jobId - ID of the job to fork
|
|
1326
|
+
* @param modifications - Optional modifications to apply to the forked job
|
|
1327
|
+
* @returns Result of the fork operation including the new job ID
|
|
1328
|
+
* @throws {InvalidStateError} If the fleet manager is not initialized
|
|
1329
|
+
* @throws {JobNotFoundError} If the original job doesn't exist
|
|
1330
|
+
* @throws {JobForkError} If the job cannot be forked (e.g., no session)
|
|
1331
|
+
*
|
|
1332
|
+
* @example
|
|
1333
|
+
* ```typescript
|
|
1334
|
+
* // Fork with same configuration
|
|
1335
|
+
* const result = await manager.forkJob('job-2024-01-15-abc123');
|
|
1336
|
+
* console.log(`Forked to: ${result.jobId}`);
|
|
1337
|
+
*
|
|
1338
|
+
* // Fork with modified prompt
|
|
1339
|
+
* const result = await manager.forkJob('job-2024-01-15-abc123', {
|
|
1340
|
+
* prompt: 'Continue the previous task but focus on testing',
|
|
1341
|
+
* });
|
|
1342
|
+
*
|
|
1343
|
+
* // Fork with different schedule
|
|
1344
|
+
* const result = await manager.forkJob('job-2024-01-15-abc123', {
|
|
1345
|
+
* schedule: 'nightly',
|
|
1346
|
+
* });
|
|
1347
|
+
* ```
|
|
1348
|
+
*/
|
|
1349
|
+
async forkJob(jobId, modifications) {
|
|
1350
|
+
// Validate state - must be at least initialized
|
|
1351
|
+
if (this.status === "uninitialized") {
|
|
1352
|
+
throw new InvalidStateError("forkJob", this.status, ["initialized", "running", "stopped"]);
|
|
1353
|
+
}
|
|
1354
|
+
const jobsDir = join(this.stateDir, "jobs");
|
|
1355
|
+
// Get the original job
|
|
1356
|
+
const originalJob = await getJob(jobsDir, jobId, { logger: this.logger });
|
|
1357
|
+
if (!originalJob) {
|
|
1358
|
+
throw new JobForkError(jobId, 'job_not_found');
|
|
1359
|
+
}
|
|
1360
|
+
// Verify the agent exists in config
|
|
1361
|
+
const agents = this.config?.agents ?? [];
|
|
1362
|
+
const agent = agents.find((a) => a.name === originalJob.agent);
|
|
1363
|
+
if (!agent) {
|
|
1364
|
+
throw new JobForkError(jobId, 'agent_not_found', {
|
|
1365
|
+
message: `Agent "${originalJob.agent}" for job "${jobId}" not found in current configuration`,
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
// Determine the prompt to use (priority: modifications > original job)
|
|
1369
|
+
const prompt = modifications?.prompt ?? originalJob.prompt ?? undefined;
|
|
1370
|
+
// Determine the schedule to use
|
|
1371
|
+
const scheduleName = modifications?.schedule ?? originalJob.schedule ?? undefined;
|
|
1372
|
+
// Create the new job
|
|
1373
|
+
const timestamp = new Date().toISOString();
|
|
1374
|
+
const newJob = await createJob(jobsDir, {
|
|
1375
|
+
agent: originalJob.agent,
|
|
1376
|
+
trigger_type: "fork",
|
|
1377
|
+
schedule: scheduleName ?? null,
|
|
1378
|
+
prompt: prompt ?? null,
|
|
1379
|
+
forked_from: jobId,
|
|
1380
|
+
});
|
|
1381
|
+
this.logger.info(`Forked job ${jobId} to new job ${newJob.id} for agent ${originalJob.agent}`);
|
|
1382
|
+
// Emit job:created event
|
|
1383
|
+
this.emit("job:created", {
|
|
1384
|
+
job: newJob,
|
|
1385
|
+
agentName: originalJob.agent,
|
|
1386
|
+
scheduleName: scheduleName ?? undefined,
|
|
1387
|
+
timestamp,
|
|
1388
|
+
});
|
|
1389
|
+
// Emit job:forked event
|
|
1390
|
+
this.emit("job:forked", {
|
|
1391
|
+
job: newJob,
|
|
1392
|
+
originalJob,
|
|
1393
|
+
agentName: originalJob.agent,
|
|
1394
|
+
timestamp,
|
|
1395
|
+
});
|
|
1396
|
+
return {
|
|
1397
|
+
jobId: newJob.id,
|
|
1398
|
+
forkedFromJobId: jobId,
|
|
1399
|
+
agentName: originalJob.agent,
|
|
1400
|
+
startedAt: newJob.started_at,
|
|
1401
|
+
prompt,
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
// ===========================================================================
|
|
1405
|
+
// Log Streaming (US-11)
|
|
1406
|
+
// ===========================================================================
|
|
1407
|
+
/**
|
|
1408
|
+
* Stream all fleet logs as an async iterable
|
|
1409
|
+
*
|
|
1410
|
+
* Provides a unified stream of logs from all sources in the fleet including
|
|
1411
|
+
* agents, jobs, and the scheduler. Logs can be filtered by level and optionally
|
|
1412
|
+
* by agent or job.
|
|
1413
|
+
*
|
|
1414
|
+
* For completed jobs, this will replay their history (if includeHistory is true)
|
|
1415
|
+
* before streaming new logs from running jobs.
|
|
1416
|
+
*
|
|
1417
|
+
* @param options - Options for filtering and configuring the stream
|
|
1418
|
+
* @returns An async iterable of LogEntry objects
|
|
1419
|
+
*
|
|
1420
|
+
* @example
|
|
1421
|
+
* ```typescript
|
|
1422
|
+
* // Stream all info+ logs
|
|
1423
|
+
* for await (const log of manager.streamLogs()) {
|
|
1424
|
+
* console.log(`[${log.level}] ${log.message}`);
|
|
1425
|
+
* }
|
|
1426
|
+
*
|
|
1427
|
+
* // Stream only errors for a specific agent
|
|
1428
|
+
* for await (const log of manager.streamLogs({
|
|
1429
|
+
* level: 'error',
|
|
1430
|
+
* agentName: 'my-agent',
|
|
1431
|
+
* })) {
|
|
1432
|
+
* console.error(log.message);
|
|
1433
|
+
* }
|
|
1434
|
+
* ```
|
|
1435
|
+
*/
|
|
1436
|
+
async *streamLogs(options) {
|
|
1437
|
+
const level = options?.level ?? "info";
|
|
1438
|
+
const includeHistory = options?.includeHistory ?? true;
|
|
1439
|
+
const historyLimit = options?.historyLimit ?? 1000;
|
|
1440
|
+
const agentFilter = options?.agentName;
|
|
1441
|
+
const jobFilter = options?.jobId;
|
|
1442
|
+
const jobsDir = join(this.stateDir, "jobs");
|
|
1443
|
+
const { readJobOutputAll } = await import("../state/job-output.js");
|
|
1444
|
+
// Replay historical logs if requested
|
|
1445
|
+
if (includeHistory) {
|
|
1446
|
+
// Get jobs to replay history from
|
|
1447
|
+
const jobsResult = await listJobs(jobsDir, agentFilter ? { agent: agentFilter } : {}, { logger: this.logger });
|
|
1448
|
+
// Filter by job ID if specified
|
|
1449
|
+
let jobs = jobsResult.jobs;
|
|
1450
|
+
if (jobFilter) {
|
|
1451
|
+
jobs = jobs.filter((j) => j.id === jobFilter);
|
|
1452
|
+
}
|
|
1453
|
+
// Sort by started_at ascending to replay in chronological order
|
|
1454
|
+
jobs.sort((a, b) => new Date(a.started_at).getTime() - new Date(b.started_at).getTime());
|
|
1455
|
+
let yielded = 0;
|
|
1456
|
+
for (const job of jobs) {
|
|
1457
|
+
if (yielded >= historyLimit)
|
|
1458
|
+
break;
|
|
1459
|
+
// Read job output and convert to log entries
|
|
1460
|
+
const output = await readJobOutputAll(jobsDir, job.id, {
|
|
1461
|
+
skipInvalidLines: true,
|
|
1462
|
+
logger: this.logger,
|
|
1463
|
+
});
|
|
1464
|
+
for (const msg of output) {
|
|
1465
|
+
if (yielded >= historyLimit)
|
|
1466
|
+
break;
|
|
1467
|
+
const logEntry = this.jobOutputToLogEntry(job, msg);
|
|
1468
|
+
if (this.shouldYieldLog(logEntry, level, agentFilter, jobFilter)) {
|
|
1469
|
+
yield logEntry;
|
|
1470
|
+
yielded++;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
// For running jobs, subscribe to job:output events
|
|
1476
|
+
const outputQueue = [];
|
|
1477
|
+
let resolveWait = null;
|
|
1478
|
+
let stopped = false;
|
|
1479
|
+
const outputHandler = (payload) => {
|
|
1480
|
+
if (stopped)
|
|
1481
|
+
return;
|
|
1482
|
+
const logEntry = {
|
|
1483
|
+
timestamp: payload.timestamp,
|
|
1484
|
+
level: "info",
|
|
1485
|
+
source: "job",
|
|
1486
|
+
agentName: payload.agentName,
|
|
1487
|
+
jobId: payload.jobId,
|
|
1488
|
+
message: payload.output,
|
|
1489
|
+
};
|
|
1490
|
+
if (this.shouldYieldLog(logEntry, level, agentFilter, jobFilter)) {
|
|
1491
|
+
outputQueue.push(logEntry);
|
|
1492
|
+
if (resolveWait) {
|
|
1493
|
+
resolveWait();
|
|
1494
|
+
resolveWait = null;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
};
|
|
1498
|
+
this.on("job:output", outputHandler);
|
|
1499
|
+
try {
|
|
1500
|
+
// Yield queued entries as they arrive
|
|
1501
|
+
while (!stopped) {
|
|
1502
|
+
while (outputQueue.length > 0) {
|
|
1503
|
+
const entry = outputQueue.shift();
|
|
1504
|
+
yield entry;
|
|
1505
|
+
}
|
|
1506
|
+
// Wait for more entries
|
|
1507
|
+
await new Promise((resolve) => {
|
|
1508
|
+
resolveWait = resolve;
|
|
1509
|
+
// Add timeout to prevent hanging forever
|
|
1510
|
+
setTimeout(resolve, 1000);
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
finally {
|
|
1515
|
+
stopped = true;
|
|
1516
|
+
this.off("job:output", outputHandler);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* Stream output from a specific job as an async iterable
|
|
1521
|
+
*
|
|
1522
|
+
* Provides a stream of log entries for a specific job. For completed jobs,
|
|
1523
|
+
* this will replay the job's history and then complete. For running jobs,
|
|
1524
|
+
* it will continue streaming until the job completes.
|
|
1525
|
+
*
|
|
1526
|
+
* @param jobId - The ID of the job to stream output from
|
|
1527
|
+
* @returns An async iterable of LogEntry objects
|
|
1528
|
+
* @throws {JobNotFoundError} If the job doesn't exist
|
|
1529
|
+
*
|
|
1530
|
+
* @example
|
|
1531
|
+
* ```typescript
|
|
1532
|
+
* // Stream job output
|
|
1533
|
+
* for await (const log of manager.streamJobOutput('job-2024-01-15-abc123')) {
|
|
1534
|
+
* console.log(`[${log.level}] ${log.message}`);
|
|
1535
|
+
* }
|
|
1536
|
+
* ```
|
|
1537
|
+
*/
|
|
1538
|
+
async *streamJobOutput(jobId) {
|
|
1539
|
+
const jobsDir = join(this.stateDir, "jobs");
|
|
1540
|
+
// Verify job exists
|
|
1541
|
+
const job = await getJob(jobsDir, jobId, { logger: this.logger });
|
|
1542
|
+
if (!job) {
|
|
1543
|
+
throw new JobNotFoundError(jobId);
|
|
1544
|
+
}
|
|
1545
|
+
const { readJobOutputAll, getJobOutputPath } = await import("../state/job-output.js");
|
|
1546
|
+
const { watch } = await import("node:fs");
|
|
1547
|
+
const { stat } = await import("node:fs/promises");
|
|
1548
|
+
const { createReadStream } = await import("node:fs");
|
|
1549
|
+
const { createInterface } = await import("node:readline");
|
|
1550
|
+
const outputPath = getJobOutputPath(jobsDir, jobId);
|
|
1551
|
+
// First, replay all existing output
|
|
1552
|
+
const existingOutput = await readJobOutputAll(jobsDir, jobId, {
|
|
1553
|
+
skipInvalidLines: true,
|
|
1554
|
+
logger: this.logger,
|
|
1555
|
+
});
|
|
1556
|
+
for (const msg of existingOutput) {
|
|
1557
|
+
yield this.jobOutputToLogEntry(job, msg);
|
|
1558
|
+
}
|
|
1559
|
+
// If job is already completed, we're done
|
|
1560
|
+
if (job.status !== "running" && job.status !== "pending") {
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
// For running jobs, watch for new output
|
|
1564
|
+
const outputQueue = [];
|
|
1565
|
+
let resolveWait = null;
|
|
1566
|
+
let stopped = false;
|
|
1567
|
+
let lastReadPosition = 0;
|
|
1568
|
+
// Get current file position
|
|
1569
|
+
try {
|
|
1570
|
+
const stats = await stat(outputPath);
|
|
1571
|
+
lastReadPosition = stats.size;
|
|
1572
|
+
}
|
|
1573
|
+
catch {
|
|
1574
|
+
// File doesn't exist yet
|
|
1575
|
+
}
|
|
1576
|
+
// Watch for file changes
|
|
1577
|
+
let watcher = null;
|
|
1578
|
+
try {
|
|
1579
|
+
watcher = watch(outputPath, async (eventType) => {
|
|
1580
|
+
if (stopped || eventType !== "change")
|
|
1581
|
+
return;
|
|
1582
|
+
try {
|
|
1583
|
+
const currentStats = await stat(outputPath);
|
|
1584
|
+
if (currentStats.size > lastReadPosition) {
|
|
1585
|
+
// Read new content
|
|
1586
|
+
const fileStream = createReadStream(outputPath, {
|
|
1587
|
+
encoding: "utf-8",
|
|
1588
|
+
start: lastReadPosition,
|
|
1589
|
+
});
|
|
1590
|
+
const rl = createInterface({
|
|
1591
|
+
input: fileStream,
|
|
1592
|
+
crlfDelay: Infinity,
|
|
1593
|
+
});
|
|
1594
|
+
for await (const line of rl) {
|
|
1595
|
+
if (stopped)
|
|
1596
|
+
break;
|
|
1597
|
+
const trimmedLine = line.trim();
|
|
1598
|
+
if (trimmedLine === "")
|
|
1599
|
+
continue;
|
|
1600
|
+
try {
|
|
1601
|
+
const parsed = JSON.parse(trimmedLine);
|
|
1602
|
+
const logEntry = this.jobOutputToLogEntry(job, parsed);
|
|
1603
|
+
outputQueue.push(logEntry);
|
|
1604
|
+
if (resolveWait) {
|
|
1605
|
+
resolveWait();
|
|
1606
|
+
resolveWait = null;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
catch {
|
|
1610
|
+
// Skip malformed lines
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
rl.close();
|
|
1614
|
+
fileStream.destroy();
|
|
1615
|
+
lastReadPosition = currentStats.size;
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
catch (err) {
|
|
1619
|
+
this.logger.warn(`Error reading output file: ${err.message}`);
|
|
1620
|
+
}
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
catch {
|
|
1624
|
+
// Can't watch file - might not exist yet
|
|
1625
|
+
}
|
|
1626
|
+
// Poll for job completion
|
|
1627
|
+
const checkJobComplete = async () => {
|
|
1628
|
+
const currentJob = await getJob(jobsDir, jobId, { logger: this.logger });
|
|
1629
|
+
return (!currentJob ||
|
|
1630
|
+
(currentJob.status !== "running" && currentJob.status !== "pending"));
|
|
1631
|
+
};
|
|
1632
|
+
try {
|
|
1633
|
+
// Yield queued entries as they arrive
|
|
1634
|
+
while (!stopped) {
|
|
1635
|
+
while (outputQueue.length > 0) {
|
|
1636
|
+
const entry = outputQueue.shift();
|
|
1637
|
+
yield entry;
|
|
1638
|
+
}
|
|
1639
|
+
// Check if job is complete
|
|
1640
|
+
if (await checkJobComplete()) {
|
|
1641
|
+
stopped = true;
|
|
1642
|
+
break;
|
|
1643
|
+
}
|
|
1644
|
+
// Wait for more entries
|
|
1645
|
+
await new Promise((resolve) => {
|
|
1646
|
+
resolveWait = resolve;
|
|
1647
|
+
setTimeout(resolve, 1000);
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
finally {
|
|
1652
|
+
stopped = true;
|
|
1653
|
+
if (watcher) {
|
|
1654
|
+
watcher.close();
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Stream logs for a specific agent as an async iterable
|
|
1660
|
+
*
|
|
1661
|
+
* Provides a stream of log entries for all jobs belonging to a specific agent.
|
|
1662
|
+
* For completed jobs, this will replay their history. For running jobs, it
|
|
1663
|
+
* will continue streaming until the iterator is stopped.
|
|
1664
|
+
*
|
|
1665
|
+
* @param agentName - The name of the agent to stream logs for
|
|
1666
|
+
* @returns An async iterable of LogEntry objects
|
|
1667
|
+
* @throws {AgentNotFoundError} If the agent doesn't exist in the configuration
|
|
1668
|
+
*
|
|
1669
|
+
* @example
|
|
1670
|
+
* ```typescript
|
|
1671
|
+
* // Stream all logs for an agent
|
|
1672
|
+
* for await (const log of manager.streamAgentLogs('my-agent')) {
|
|
1673
|
+
* console.log(`[${log.jobId}] ${log.message}`);
|
|
1674
|
+
* }
|
|
1675
|
+
* ```
|
|
1676
|
+
*/
|
|
1677
|
+
async *streamAgentLogs(agentName) {
|
|
1678
|
+
// Verify agent exists
|
|
1679
|
+
const agents = this.config?.agents ?? [];
|
|
1680
|
+
const agent = agents.find((a) => a.name === agentName);
|
|
1681
|
+
if (!agent) {
|
|
1682
|
+
throw new AgentNotFoundError(agentName, {
|
|
1683
|
+
availableAgents: agents.map((a) => a.name),
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
// Delegate to streamLogs with agent filter
|
|
1687
|
+
yield* this.streamLogs({
|
|
1688
|
+
agentName,
|
|
1689
|
+
includeHistory: true,
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
// ===========================================================================
|
|
1693
|
+
// Log Streaming Helpers (US-11)
|
|
1694
|
+
// ===========================================================================
|
|
1695
|
+
/**
|
|
1696
|
+
* Convert a job output message to a LogEntry
|
|
1697
|
+
*/
|
|
1698
|
+
jobOutputToLogEntry(job, msg) {
|
|
1699
|
+
// Determine log level based on message type
|
|
1700
|
+
let level = "info";
|
|
1701
|
+
if (msg.type === "error") {
|
|
1702
|
+
level = "error";
|
|
1703
|
+
}
|
|
1704
|
+
else if (msg.type === "system") {
|
|
1705
|
+
level = "debug";
|
|
1706
|
+
}
|
|
1707
|
+
return {
|
|
1708
|
+
timestamp: msg.timestamp ?? new Date().toISOString(),
|
|
1709
|
+
level,
|
|
1710
|
+
source: "job",
|
|
1711
|
+
agentName: job.agent,
|
|
1712
|
+
jobId: job.id,
|
|
1713
|
+
scheduleName: job.schedule ?? undefined,
|
|
1714
|
+
message: msg.content ?? "",
|
|
1715
|
+
data: { type: msg.type },
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
/**
|
|
1719
|
+
* Determine if a log entry should be yielded based on filters
|
|
1720
|
+
*/
|
|
1721
|
+
shouldYieldLog(entry, minLevel, agentFilter, jobFilter) {
|
|
1722
|
+
// Check log level
|
|
1723
|
+
const levelOrder = {
|
|
1724
|
+
debug: 0,
|
|
1725
|
+
info: 1,
|
|
1726
|
+
warn: 2,
|
|
1727
|
+
error: 3,
|
|
1728
|
+
};
|
|
1729
|
+
if (levelOrder[entry.level] < levelOrder[minLevel]) {
|
|
1730
|
+
return false;
|
|
1731
|
+
}
|
|
1732
|
+
// Check agent filter
|
|
1733
|
+
if (agentFilter && entry.agentName !== agentFilter) {
|
|
1734
|
+
return false;
|
|
1735
|
+
}
|
|
1736
|
+
// Check job filter
|
|
1737
|
+
if (jobFilter && entry.jobId !== jobFilter) {
|
|
1738
|
+
return false;
|
|
1739
|
+
}
|
|
1740
|
+
return true;
|
|
1741
|
+
}
|
|
1742
|
+
// ===========================================================================
|
|
1743
|
+
// Private Helper Methods
|
|
1744
|
+
// ===========================================================================
|
|
1745
|
+
/**
|
|
1746
|
+
* Load configuration with proper error handling
|
|
1747
|
+
*/
|
|
1748
|
+
async loadConfiguration() {
|
|
1749
|
+
try {
|
|
1750
|
+
return await loadConfig(this.configPath);
|
|
1751
|
+
}
|
|
1752
|
+
catch (error) {
|
|
1753
|
+
if (error instanceof ConfigNotFoundError) {
|
|
1754
|
+
throw new FleetManagerConfigError(`Configuration file not found. ${error.message}`, this.configPath, { cause: error });
|
|
1755
|
+
}
|
|
1756
|
+
if (error instanceof ConfigError) {
|
|
1757
|
+
throw new FleetManagerConfigError(`Invalid configuration: ${error.message}`, this.configPath, { cause: error });
|
|
1758
|
+
}
|
|
1759
|
+
throw new FleetManagerConfigError(`Failed to load configuration: ${error instanceof Error ? error.message : String(error)}`, this.configPath, { cause: error instanceof Error ? error : undefined });
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
/**
|
|
1763
|
+
* Initialize state directory with proper error handling
|
|
1764
|
+
*/
|
|
1765
|
+
async initializeStateDir() {
|
|
1766
|
+
try {
|
|
1767
|
+
return await initStateDirectory({ path: this.stateDir });
|
|
1768
|
+
}
|
|
1769
|
+
catch (error) {
|
|
1770
|
+
throw new FleetManagerStateDirError(`Failed to initialize state directory: ${error instanceof Error ? error.message : String(error)}`, this.stateDir, { cause: error instanceof Error ? error : undefined });
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
/**
|
|
1774
|
+
* Start the scheduler asynchronously (don't block on the loop)
|
|
1775
|
+
*/
|
|
1776
|
+
startSchedulerAsync(agents) {
|
|
1777
|
+
// Start the scheduler loop in the background
|
|
1778
|
+
// The scheduler.start() method runs the loop and returns when stopped
|
|
1779
|
+
this.scheduler.start(agents).catch((error) => {
|
|
1780
|
+
// Only handle errors if we're still supposed to be running
|
|
1781
|
+
if (this.status === "running" || this.status === "starting") {
|
|
1782
|
+
this.logger.error(`Scheduler error: ${error instanceof Error ? error.message : String(error)}`);
|
|
1783
|
+
this.status = "error";
|
|
1784
|
+
this.lastError = error instanceof Error ? error.message : String(error);
|
|
1785
|
+
this.emit("error", error instanceof Error ? error : new Error(String(error)));
|
|
1786
|
+
}
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
/**
|
|
1790
|
+
* Handle schedule trigger callback from scheduler
|
|
1791
|
+
*/
|
|
1792
|
+
async handleScheduleTrigger(info) {
|
|
1793
|
+
const { agent, scheduleName, schedule } = info;
|
|
1794
|
+
const timestamp = new Date().toISOString();
|
|
1795
|
+
this.logger.info(`Triggering ${agent.name}/${scheduleName}`);
|
|
1796
|
+
// Emit legacy event for backwards compatibility
|
|
1797
|
+
this.emit("schedule:trigger", agent.name, scheduleName);
|
|
1798
|
+
// Emit new typed event with full payload
|
|
1799
|
+
this.emit("schedule:triggered", {
|
|
1800
|
+
agentName: agent.name,
|
|
1801
|
+
scheduleName,
|
|
1802
|
+
schedule,
|
|
1803
|
+
timestamp,
|
|
1804
|
+
});
|
|
1805
|
+
try {
|
|
1806
|
+
// For now, just log the trigger
|
|
1807
|
+
// In future PRDs, this will actually run the agent via the runner
|
|
1808
|
+
this.logger.debug(`Schedule ${scheduleName} triggered for agent ${agent.name} ` +
|
|
1809
|
+
`(type: ${schedule.type}, prompt: ${schedule.prompt ?? "default"})`);
|
|
1810
|
+
// Emit legacy completion event for backwards compatibility
|
|
1811
|
+
this.emit("schedule:complete", agent.name, scheduleName);
|
|
1812
|
+
}
|
|
1813
|
+
catch (error) {
|
|
1814
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1815
|
+
this.logger.error(`Error in ${agent.name}/${scheduleName}: ${err.message}`);
|
|
1816
|
+
// Emit legacy error event for backwards compatibility
|
|
1817
|
+
this.emit("schedule:error", agent.name, scheduleName, err);
|
|
1818
|
+
throw error;
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
// ===========================================================================
|
|
1822
|
+
// Event Emission Helpers (US-2)
|
|
1823
|
+
// ===========================================================================
|
|
1824
|
+
/**
|
|
1825
|
+
* Emit a config:reloaded event
|
|
1826
|
+
*
|
|
1827
|
+
* Called when configuration is hot-reloaded.
|
|
1828
|
+
*/
|
|
1829
|
+
emitConfigReloaded(payload) {
|
|
1830
|
+
this.emit("config:reloaded", payload);
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* Emit an agent:started event
|
|
1834
|
+
*
|
|
1835
|
+
* Called when an agent is started/registered with the fleet.
|
|
1836
|
+
*/
|
|
1837
|
+
emitAgentStarted(payload) {
|
|
1838
|
+
this.emit("agent:started", payload);
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* Emit an agent:stopped event
|
|
1842
|
+
*
|
|
1843
|
+
* Called when an agent is stopped/unregistered from the fleet.
|
|
1844
|
+
*/
|
|
1845
|
+
emitAgentStopped(payload) {
|
|
1846
|
+
this.emit("agent:stopped", payload);
|
|
1847
|
+
}
|
|
1848
|
+
/**
|
|
1849
|
+
* Emit a schedule:skipped event
|
|
1850
|
+
*
|
|
1851
|
+
* Called when a schedule check is skipped (already running, disabled, etc.).
|
|
1852
|
+
*/
|
|
1853
|
+
emitScheduleSkipped(payload) {
|
|
1854
|
+
this.emit("schedule:skipped", payload);
|
|
1855
|
+
}
|
|
1856
|
+
/**
|
|
1857
|
+
* Emit a job:created event
|
|
1858
|
+
*
|
|
1859
|
+
* Called when a new job is created.
|
|
1860
|
+
*/
|
|
1861
|
+
emitJobCreated(payload) {
|
|
1862
|
+
this.emit("job:created", payload);
|
|
1863
|
+
}
|
|
1864
|
+
/**
|
|
1865
|
+
* Emit a job:output event
|
|
1866
|
+
*
|
|
1867
|
+
* Called when a job produces output during execution.
|
|
1868
|
+
* This enables real-time streaming of output to UIs.
|
|
1869
|
+
*/
|
|
1870
|
+
emitJobOutput(payload) {
|
|
1871
|
+
this.emit("job:output", payload);
|
|
1872
|
+
}
|
|
1873
|
+
/**
|
|
1874
|
+
* Emit a job:completed event
|
|
1875
|
+
*
|
|
1876
|
+
* Called when a job completes successfully.
|
|
1877
|
+
*/
|
|
1878
|
+
emitJobCompleted(payload) {
|
|
1879
|
+
this.emit("job:completed", payload);
|
|
1880
|
+
}
|
|
1881
|
+
/**
|
|
1882
|
+
* Emit a job:failed event
|
|
1883
|
+
*
|
|
1884
|
+
* Called when a job fails.
|
|
1885
|
+
*/
|
|
1886
|
+
emitJobFailed(payload) {
|
|
1887
|
+
this.emit("job:failed", payload);
|
|
1888
|
+
}
|
|
1889
|
+
/**
|
|
1890
|
+
* Emit a job:cancelled event (US-6)
|
|
1891
|
+
*
|
|
1892
|
+
* Called when a job is cancelled.
|
|
1893
|
+
*/
|
|
1894
|
+
emitJobCancelled(payload) {
|
|
1895
|
+
this.emit("job:cancelled", payload);
|
|
1896
|
+
}
|
|
1897
|
+
/**
|
|
1898
|
+
* Emit a job:forked event (US-6)
|
|
1899
|
+
*
|
|
1900
|
+
* Called when a job is forked to create a new job.
|
|
1901
|
+
*/
|
|
1902
|
+
emitJobForked(payload) {
|
|
1903
|
+
this.emit("job:forked", payload);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
//# sourceMappingURL=fleet-manager.js.map
|