@oisincoveney/pipeline 3.24.2 → 4.0.0
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/README.md +14 -2
- package/defaults/pipeline.yaml +23 -5
- package/defaults/profiles.yaml +104 -21
- package/dist/argo-graph.js +90 -88
- package/dist/argo-submit.d.ts +110 -138
- package/dist/argo-submit.js +363 -378
- package/dist/argo-workflow.d.ts +156 -177
- package/dist/argo-workflow.js +96 -138
- package/dist/backlog.js +5 -4
- package/dist/bench/eval-report.js +21 -12
- package/dist/claude-settings-config.js +23 -31
- package/dist/claude-user-config.js +6 -16
- package/dist/cli/bootstrap-commands.js +3 -3
- package/dist/cli/doctor.d.ts +1 -1
- package/dist/cli/doctor.js +176 -171
- package/dist/cli/factory-commands.js +13 -13
- package/dist/cli/format.js +92 -86
- package/dist/cli/loop-commands.js +21 -21
- package/dist/cli/mcp-gateway-commands.js +12 -12
- package/dist/cli/plan-commands.js +85 -116
- package/dist/cli/program.d.ts +3 -3
- package/dist/cli/program.js +41 -43
- package/dist/cli/run-command.d.ts +1 -1
- package/dist/cli/run-command.js +25 -25
- package/dist/cli/run-commands.js +31 -29
- package/dist/cli/run-resolver.d.ts +1 -1
- package/dist/cli/run-resolver.js +30 -32
- package/dist/cli/run-service.d.ts +2 -3
- package/dist/cli/run-service.js +229 -239
- package/dist/cli/submit-options.js +85 -83
- package/dist/cluster-doctor.js +147 -175
- package/dist/codex-config.js +24 -30
- package/dist/commands/bench-command.js +9 -7
- package/dist/commands/coordinator-command.js +35 -0
- package/dist/commands/pipeline-command.js +13 -7
- package/dist/commands/runner-command-command.js +39 -24
- package/dist/commands/runner-node-command.js +31 -0
- package/dist/commands/ticket/complete.js +29 -33
- package/dist/commands/ticket/create.js +84 -82
- package/dist/commands/ticket/graph-check.js +11 -11
- package/dist/commands/ticket/next.js +36 -36
- package/dist/commands/ticket/sequence.js +11 -12
- package/dist/commands/ticket/shared.d.ts +5 -2
- package/dist/commands/ticket/shared.js +63 -66
- package/dist/commands/ticket/start.js +58 -67
- package/dist/commands/ticket-command.js +2 -2
- package/dist/config/defaults.d.ts +139 -140
- package/dist/config/defaults.js +81 -80
- package/dist/config/lint.js +52 -64
- package/dist/config/load.d.ts +4 -5
- package/dist/config/load.js +64 -74
- package/dist/config/schema/catalog.d.ts +2 -1
- package/dist/config/schema/catalog.js +3 -2
- package/dist/config/schema/mcp.js +46 -52
- package/dist/config/schemas.d.ts +382 -528
- package/dist/config/schemas.js +400 -398
- package/dist/config/validate.d.ts +1 -2
- package/dist/config/validate.js +236 -201
- package/dist/config.d.ts +2 -2
- package/dist/context/repo-map.js +105 -119
- package/dist/coordinator/composition.js +169 -0
- package/dist/coordinator/config.js +71 -0
- package/dist/coordinator/run-owner.js +64 -0
- package/dist/coordinator/wave-advance.js +148 -0
- package/dist/coordinator/wave-scheduler.js +50 -0
- package/dist/credentials/broker.d.ts +6 -8
- package/dist/credentials/broker.js +17 -16
- package/dist/credentials/codex-config.js +15 -19
- package/dist/credentials/file-targets.js +9 -15
- package/dist/credentials/local-codex-auth-sync.js +55 -58
- package/dist/credentials/opencode-config.js +28 -32
- package/dist/credentials/runner.js +13 -13
- package/dist/effect-sync-errors.js +28 -0
- package/dist/factory/create-experiment.js +285 -121
- package/dist/factory/exec.js +10 -12
- package/dist/factory/git-credentials.js +4 -4
- package/dist/factory/stamp-answers.js +10 -11
- package/dist/factory/template-update.js +65 -44
- package/dist/factory-lane.d.ts +26 -40
- package/dist/factory-lane.js +26 -25
- package/dist/gates.js +3 -3
- package/dist/git-remote-url.js +17 -19
- package/dist/hooks.d.ts +26 -26
- package/dist/hooks.js +20 -22
- package/dist/index.d.ts +4 -3
- package/dist/index.js +27 -36
- package/dist/install-commands/claude-code.js +100 -92
- package/dist/install-commands/host-selection.js +3 -6
- package/dist/install-commands/opencode.js +264 -259
- package/dist/install-commands/planner.js +163 -181
- package/dist/install-commands/result-format.js +1 -3
- package/dist/install-commands/shared.js +25 -42
- package/dist/install-commands/writer.js +22 -25
- package/dist/install-commands.js +8 -8
- package/dist/jscpd-output.js +29 -0
- package/dist/json-config-merge.js +14 -20
- package/dist/json-line-values.js +17 -6
- package/dist/loop/argo-poll.d.ts +0 -1
- package/dist/loop/argo-poll.js +15 -95
- package/dist/loop/backlog-records.js +1 -3
- package/dist/loop/controller-deps.js +150 -161
- package/dist/loop/controller.js +164 -186
- package/dist/loop/gh-checks.js +62 -61
- package/dist/loop/gh-runner.js +22 -25
- package/dist/loop/loop-command.js +66 -58
- package/dist/loop/loop-controller-entrypoint.js +29 -30
- package/dist/loop/merge.js +36 -40
- package/dist/loop/run-terminal-poll.js +69 -0
- package/dist/mcp/gateway-config.js +25 -29
- package/dist/mcp/gateway-doctor.js +77 -119
- package/dist/mcp/gateway-reconcile.js +17 -22
- package/dist/mcp/gateway-runtime.js +1 -3
- package/dist/mcp/host-config.js +37 -40
- package/dist/mcp/host-renderers.js +10 -18
- package/dist/mcp/repo-local-backends.js +51 -54
- package/dist/mcp/toolhive-vmcp.js +39 -39
- package/dist/model-resolver.js +29 -33
- package/dist/moka-global-config.d.ts +78 -55
- package/dist/moka-global-config.js +93 -92
- package/dist/moka-submit.d.ts +2179 -866
- package/dist/moka-submit.js +97 -99
- package/dist/opencode-project-config.js +34 -40
- package/dist/package-assets.js +1 -3
- package/dist/path-refs.js +2 -2
- package/dist/pipeline-init.js +25 -25
- package/dist/pipeline-runtime.d.ts +15 -14
- package/dist/pipeline-runtime.js +51 -50
- package/dist/planned-node.js +6 -3
- package/dist/planning/compile.d.ts +5 -5
- package/dist/planning/compile.js +140 -139
- package/dist/planning/dependency-refs.js +5 -5
- package/dist/planning/generate.d.ts +1296 -1296
- package/dist/planning/generate.js +307 -314
- package/dist/planning/graph.d.ts +0 -1
- package/dist/planning/graph.js +131 -149
- package/dist/remote/argo/model.d.ts +104 -122
- package/dist/remote/argo/model.js +205 -197
- package/dist/remote/argo/policy.js +39 -47
- package/dist/remote/argo/storage.js +81 -85
- package/dist/remote/jobs/execution-identity.js +70 -0
- package/dist/remote/jobs/job-outcome.js +108 -0
- package/dist/remote/jobs/manifest.js +96 -0
- package/dist/remote/jobs/model.d.ts +158 -0
- package/dist/remote/jobs/model.js +162 -0
- package/dist/remote/jobs/node-bootstrap.js +213 -0
- package/dist/remote/jobs/policy.js +123 -0
- package/dist/remote/jobs/reconcile.js +100 -0
- package/dist/remote/jobs/run-spec-client.js +64 -0
- package/dist/remote/jobs/run-spec-path.js +31 -0
- package/dist/remote/jobs/run-spec-server.d.ts +34 -0
- package/dist/remote/jobs/run-spec-server.js +116 -0
- package/dist/remote/jobs/submit.d.ts +23 -0
- package/dist/remote/jobs/submit.js +116 -0
- package/dist/remote/jobs/watch.js +59 -0
- package/dist/remote/submit/argo-submission.d.ts +3 -1
- package/dist/remote/submit/argo-submission.js +53 -58
- package/dist/remote/submit/compilation.d.ts +2 -0
- package/dist/remote/submit/compilation.js +46 -49
- package/dist/remote/submit/event-boundary.js +62 -76
- package/dist/remote/submit/hook-events.d.ts +6 -0
- package/dist/remote/submit/io.d.ts +7 -1
- package/dist/remote/submit/io.js +40 -43
- package/dist/remote/submit/jobs-submission.d.ts +59 -0
- package/dist/remote/submit/jobs-submission.js +97 -0
- package/dist/remote/submit/jobs-submit-result.d.ts +25 -0
- package/dist/remote/submit/jobs-submit-result.js +34 -0
- package/dist/remote/submit/options.d.ts +49 -0
- package/dist/remote/submit/options.js +27 -0
- package/dist/remote/submit/service.d.ts +40 -1
- package/dist/remote/submit/service.js +67 -49
- package/dist/run-control/command-context.js +6 -8
- package/dist/run-control/commands.js +10 -10
- package/dist/run-control/contracts.d.ts +52 -101
- package/dist/run-control/contracts.js +83 -69
- package/dist/run-control/detach.js +66 -72
- package/dist/run-control/execution-store.js +0 -0
- package/dist/run-control/file-errors.js +1 -3
- package/dist/run-control/logical-segment.js +3 -3
- package/dist/run-control/next-node.js +67 -78
- package/dist/run-control/postgres/postgres-run-control-store.js +196 -225
- package/dist/run-control/resume-command.js +34 -41
- package/dist/run-control/run-artifacts-command.js +84 -105
- package/dist/run-control/run-command-domain.js +7 -11
- package/dist/run-control/run-control-store.js +9 -9
- package/dist/run-control/run-query-command.js +73 -100
- package/dist/run-control/run-record.js +5 -3
- package/dist/run-control/run-state-lock.js +5 -5
- package/dist/run-control/runtime-event-projection.js +38 -50
- package/dist/run-control/runtime-reporter.js +86 -102
- package/dist/run-control/stop-command.js +42 -53
- package/dist/run-control/store-fs-effects.js +3 -1
- package/dist/run-control/store-manifest.js +38 -13
- package/dist/run-control/store-paths.js +3 -3
- package/dist/run-control/store.js +1 -1
- package/dist/run-control/submit-result.js +21 -21
- package/dist/run-control/supervisor.js +78 -68
- package/dist/run-control/workspace.js +2 -2
- package/dist/run-state/git-refs.js +258 -307
- package/dist/runner/opencode-excludes.js +4 -4
- package/dist/runner/subprocess-result.js +28 -37
- package/dist/runner/subprocess.js +41 -41
- package/dist/runner/timeouts.js +14 -13
- package/dist/runner-command/dynamic-command.js +46 -39
- package/dist/runner-command/environment-commands.js +91 -0
- package/dist/runner-command/finalize.js +174 -153
- package/dist/runner-command/lifecycle-context.js +115 -130
- package/dist/runner-command/lifecycle.js +78 -72
- package/dist/runner-command/pre-schedule.js +203 -223
- package/dist/runner-command/run.js +450 -448
- package/dist/runner-command/schedule-source-options.js +8 -10
- package/dist/runner-command/select-ready-wave.js +39 -30
- package/dist/runner-command/task-descriptor.js +11 -17
- package/dist/runner-command-contract.d.ts +147 -157
- package/dist/runner-command-contract.js +403 -411
- package/dist/runner-event-schema.d.ts +364 -505
- package/dist/runner-event-schema.js +187 -175
- package/dist/runner-event-sink.js +37 -43
- package/dist/runner-output.js +6 -6
- package/dist/runner.d.ts +57 -37
- package/dist/runner.js +176 -127
- package/dist/runtime/actor-ids.d.ts +1 -0
- package/dist/runtime/actor-ids.js +2 -2
- package/dist/runtime/agent-node/agent-node.js +99 -110
- package/dist/runtime/agent-node/handoff-finalization.js +23 -24
- package/dist/runtime/agent-node/model-selection.js +19 -22
- package/dist/runtime/agent-node/output-finalization.js +174 -204
- package/dist/runtime/agent-node/prompt-rendering.js +208 -251
- package/dist/runtime/agent-node/session-execution.js +43 -39
- package/dist/runtime/builtins/builtins.js +218 -284
- package/dist/runtime/changed-files/changed-files.js +29 -34
- package/dist/runtime/command-executor/command-executor.js +31 -23
- package/dist/runtime/config-error.js +1 -3
- package/dist/runtime/context/context.js +35 -37
- package/dist/runtime/contracts/contracts.d.ts +3 -0
- package/dist/runtime/contracts/index.d.ts +1 -1
- package/dist/runtime/detached-race.js +11 -14
- package/dist/runtime/drain-merge/drain-merge.js +139 -164
- package/dist/runtime/durable-store/acquisition.js +5 -3
- package/dist/runtime/durable-store/durable-store.d.ts +3 -1
- package/dist/runtime/durable-store/postgres/client.js +29 -0
- package/dist/runtime/durable-store/postgres/migrate-substrate.js +34 -29
- package/dist/runtime/durable-store/postgres/postgres-store.js +18 -20
- package/dist/runtime/events/events.js +273 -331
- package/dist/runtime/events/index.js +2 -2
- package/dist/runtime/execution-envelope.js +44 -0
- package/dist/runtime/gates/adjudication/llm-judge.js +21 -24
- package/dist/runtime/gates/adjudication/structured-claim.js +15 -16
- package/dist/runtime/gates/adjudicator/adjudicator.js +27 -27
- package/dist/runtime/gates/adjudicator/index.js +2 -2
- package/dist/runtime/gates/contract.js +5 -9
- package/dist/runtime/gates/gates.js +9 -8
- package/dist/runtime/gates/kinds/acceptance/acceptance.js +68 -71
- package/dist/runtime/gates/kinds/acceptance/index.js +2 -2
- package/dist/runtime/gates/kinds/artifact/artifact.js +4 -4
- package/dist/runtime/gates/kinds/artifact/index.js +2 -2
- package/dist/runtime/gates/kinds/builtin/builtin.js +4 -4
- package/dist/runtime/gates/kinds/builtin/index.js +2 -2
- package/dist/runtime/gates/kinds/changed-files/changed-files.js +31 -35
- package/dist/runtime/gates/kinds/changed-files/index.js +2 -2
- package/dist/runtime/gates/kinds/command/command.js +3 -3
- package/dist/runtime/gates/kinds/command/index.js +2 -2
- package/dist/runtime/gates/kinds/json-schema/index.js +2 -2
- package/dist/runtime/gates/kinds/json-schema/json-schema.js +7 -6
- package/dist/runtime/gates/kinds/verdict/index.js +2 -2
- package/dist/runtime/gates/kinds/verdict/verdict.js +3 -3
- package/dist/runtime/gates/orchestrator.js +72 -86
- package/dist/runtime/gates/registry.js +1 -3
- package/dist/runtime/handoff.d.ts +2 -1
- package/dist/runtime/handoff.js +43 -46
- package/dist/runtime/hooks/command-hook.js +57 -71
- package/dist/runtime/hooks/context.js +21 -25
- package/dist/runtime/hooks/events.js +84 -100
- package/dist/runtime/hooks/execution.js +6 -12
- package/dist/runtime/hooks/hooks.js +34 -23
- package/dist/runtime/hooks/invocation.js +23 -29
- package/dist/runtime/hooks/module-hook.js +36 -35
- package/dist/runtime/hooks/policy.js +32 -29
- package/dist/runtime/hooks/results.js +56 -66
- package/dist/runtime/journal-acquisition.d.ts +3 -2
- package/dist/runtime/journal-acquisition.js +14 -14
- package/dist/runtime/json-validation/json-validation.js +51 -72
- package/dist/runtime/launch-plan-args.js +39 -0
- package/dist/runtime/local-scheduler.js +46 -48
- package/dist/runtime/node-execution.js +376 -445
- package/dist/runtime/node-protocol/node-protocol.js +36 -79
- package/dist/runtime/node-state-store.d.ts +4 -1
- package/dist/runtime/node-state-store.js +13 -57
- package/dist/runtime/node-state-tracker.js +46 -54
- package/dist/runtime/open-pull-request/open-pull-request.js +201 -187
- package/dist/runtime/opencode-adapter.js +23 -24
- package/dist/runtime/opencode-agent-name.js +6 -6
- package/dist/runtime/opencode-runtime.js +115 -93
- package/dist/runtime/opencode-server.js +39 -53
- package/dist/runtime/opencode-session-executor.js +312 -344
- package/dist/runtime/parallel-node/parallel-node.js +123 -134
- package/dist/runtime/parallel-worktrees/parallel-worktrees.js +90 -73
- package/dist/runtime/protected-paths/protected-paths.js +70 -71
- package/dist/runtime/remediation/remediation.js +202 -231
- package/dist/runtime/retry.js +8 -10
- package/dist/runtime/run-journal.d.ts +2 -0
- package/dist/runtime/run-journal.js +7 -7
- package/dist/runtime/runtime-results.js +54 -69
- package/dist/runtime/scheduled-dependencies.d.ts +2 -1
- package/dist/runtime/scheduled-dependencies.js +61 -75
- package/dist/runtime/scheduler.js +179 -202
- package/dist/runtime/services/agent-node-runtime-service.js +3 -3
- package/dist/runtime/services/backlog-service.d.ts +1 -2
- package/dist/runtime/services/backlog-service.js +8 -14
- package/dist/runtime/services/command-executor-service.js +1 -1
- package/dist/runtime/services/config-io-service.js +24 -21
- package/dist/runtime/services/drain-merge-git-service.js +1 -1
- package/dist/runtime/services/file-system-service.js +9 -7
- package/dist/runtime/services/git-porcelain-service.js +6 -6
- package/dist/runtime/services/kubernetes-argo-service.d.ts +10 -3
- package/dist/runtime/services/kubernetes-argo-service.js +90 -84
- package/dist/runtime/services/kubernetes-jobs-read-service.js +115 -0
- package/dist/runtime/services/kubernetes-jobs-service.d.ts +37 -0
- package/dist/runtime/services/kubernetes-jobs-service.js +45 -0
- package/dist/runtime/services/kubernetes-jobs-watch-service.js +61 -0
- package/dist/runtime/services/kubernetes-lease-service.js +21 -0
- package/dist/runtime/services/mcp-gateway-service.js +143 -94
- package/dist/runtime/services/open-pull-request-git-service.js +1 -3
- package/dist/runtime/services/opencode-runtime-server-service.js +10 -12
- package/dist/runtime/services/opencode-sdk-service.js +7 -7
- package/dist/runtime/services/repo-io-service.js +10 -12
- package/dist/runtime/services/run-journal-file-service.js +2 -2
- package/dist/runtime/services/runner-command-io-service.js +48 -52
- package/dist/runtime/services/runner-event-sink-http-service.js +32 -39
- package/dist/runtime/step/step-node.js +15 -15
- package/dist/runtime/workflow-execution.js +56 -57
- package/dist/runtime/workflow-lifecycle.js +82 -101
- package/dist/runtime/yeet-event-record.js +104 -0
- package/dist/runtime/yeet-executor.js +184 -0
- package/dist/safe-json.js +16 -16
- package/dist/schedule/backlog-context.js +122 -116
- package/dist/schedule/baseline.js +249 -255
- package/dist/schedule/passes/coverage.js +63 -68
- package/dist/schedule/passes/drain-merge.js +34 -40
- package/dist/schedule/passes/ids.js +15 -17
- package/dist/schedule/passes/models.js +40 -37
- package/dist/schedule/passes/open-pull-request.js +16 -20
- package/dist/schedule/passes/references.js +18 -22
- package/dist/schedule/prompts.js +135 -141
- package/dist/schedule/scheduling-roles.js +8 -14
- package/dist/schema-boundary.d.ts +4 -0
- package/dist/schema-boundary.js +145 -0
- package/dist/serialized-write-queue.js +2 -2
- package/dist/standard-output-schemas.js +61 -59
- package/dist/strings.js +12 -12
- package/dist/task-ref.js +5 -5
- package/dist/tickets/apply-ticket-plan.js +52 -61
- package/dist/tickets/backlog-task-store.d.ts +15 -23
- package/dist/tickets/backlog-task-store.js +82 -94
- package/dist/tickets/completion/complete-ticket.js +53 -59
- package/dist/tickets/ticket-graph-dto.d.ts +24 -48
- package/dist/tickets/ticket-graph-dto.js +40 -40
- package/dist/tickets/ticket-graph.d.ts +7 -2
- package/dist/tickets/ticket-graph.js +36 -50
- package/dist/tickets/ticket-plan-command-args.js +6 -10
- package/dist/tickets/ticket-plan-render.js +10 -16
- package/dist/tickets/ticket-plan.d.ts +32 -41
- package/dist/tickets/ticket-plan.js +97 -77
- package/dist/tickets/ticket-selection.d.ts +3 -1
- package/dist/tickets/ticket-selection.js +37 -52
- package/dist/tickets/ticket-task-index.js +13 -17
- package/dist/tickets/validation-error-format.js +3 -10
- package/dist/token-estimator.js +12 -7
- package/dist/unknown-fields.js +21 -0
- package/dist/workflow-submit-contract.d.ts +9 -10
- package/dist/workflow-submit-contract.js +10 -9
- package/docs/adr-moka-postgres-schema-isolation.md +48 -0
- package/docs/adr-opencode-first-goal-loop-runtime.md +11 -11
- package/docs/config-architecture.md +13 -13
- package/docs/mcp-host-isolation.md +1 -0
- package/docs/moka-orchestrator-design.md +3 -3
- package/docs/operator-guide.md +1 -1
- package/docs/pipeline-console-runner-contract.md +10 -6
- package/docs/runtime-actor-model.md +18 -18
- package/docs/runtime-test-subsystems.md +200 -0
- package/docs/slash-command-adapter-contract.md +3 -3
- package/package.json +49 -85
- package/dist/config/schema/reference-validation.js +0 -83
|
@@ -1,138 +1,60 @@
|
|
|
1
|
+
import { isNumberValue, isStringValue } from "../schema-boundary.js";
|
|
1
2
|
import { isRecord } from "../safe-json.js";
|
|
2
3
|
import { raceDetached } from "./detached-race.js";
|
|
4
|
+
import { promptText } from "./launch-plan-args.js";
|
|
3
5
|
import { opencodeAgentName } from "./opencode-agent-name.js";
|
|
4
6
|
import { OpencodeSdkService, OpencodeSdkServiceLive } from "./services/opencode-sdk-service.js";
|
|
5
|
-
import { Duration, Effect } from "effect";
|
|
7
|
+
import { Duration, Effect, Option } from "effect";
|
|
6
8
|
//#region src/runtime/opencode-session-executor.ts
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return Effect.gen(function* () {
|
|
21
|
-
yield* validateOpencodePlan(plan);
|
|
22
|
-
return yield* executeOpencodeSession(deps, plan, options);
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
function executeOpencodeSession(deps, plan, options) {
|
|
26
|
-
const activity = { last: Date.now() };
|
|
27
|
-
return Effect.gen(function* () {
|
|
28
|
-
return successResult(plan, yield* driveSession(deps, plan, options, activity));
|
|
29
|
-
}).pipe(boundByIdle(deps, plan, options, activity), boundByAgentTimeout(plan), Effect.catch((error) => Effect.succeed(failureResult(plan, error))));
|
|
30
|
-
}
|
|
31
|
-
function boundByAgentTimeout(plan) {
|
|
32
|
-
return (effect) => {
|
|
33
|
-
const timeoutMs = plan.timeoutMs;
|
|
34
|
-
if (!timeoutMs || timeoutMs <= 0) return effect;
|
|
35
|
-
return raceDetached(effect, timeoutFailure(timeoutMs, `agent session timed out after ${timeoutMs}ms`));
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
function boundByIdle(deps, plan, options, activity) {
|
|
39
|
-
return (effect) => {
|
|
40
|
-
const idleMs = plan.idleTimeoutMs;
|
|
41
|
-
if (!idleMs || idleMs <= 0 || !options.onOutput) return effect;
|
|
42
|
-
if (plan.timeoutMs && idleMs >= plan.timeoutMs) return effect;
|
|
43
|
-
return raceDetached(effect, idleWatchdog(deps, plan, options, activity, idleMs));
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
function timeoutFailure(milliseconds, message) {
|
|
47
|
-
return Effect.sleep(Duration.millis(milliseconds)).pipe(Effect.andThen(Effect.fail(new Error(message))));
|
|
48
|
-
}
|
|
49
|
-
function idleWatchdog(deps, plan, options, activity, idleMs) {
|
|
50
|
-
const loop = Effect.suspend(() => {
|
|
51
|
-
const remaining = idleMs - (Date.now() - activity.last);
|
|
52
|
-
if (remaining <= 0) return onIdleTimeout(deps, plan, options, idleMs);
|
|
53
|
-
return Effect.sleep(Duration.millis(remaining)).pipe(Effect.andThen(loop));
|
|
54
|
-
});
|
|
55
|
-
return loop;
|
|
56
|
-
}
|
|
57
|
-
function onIdleTimeout(deps, plan, options, idleMs) {
|
|
58
|
-
return Effect.sync(() => {
|
|
59
|
-
options.onOutput?.({
|
|
60
|
-
chunk: `opencode session idle for ${idleMs}ms (no progress); aborting\n`,
|
|
61
|
-
nodeId: plan.nodeId,
|
|
62
|
-
stream: "stderr"
|
|
63
|
-
});
|
|
64
|
-
}).pipe(Effect.andThen(abortBestEffort(deps, plan)), Effect.andThen(Effect.fail(/* @__PURE__ */ new Error(`agent session idle for ${idleMs}ms (no progress)`))));
|
|
65
|
-
}
|
|
66
|
-
function abortBestEffort(deps, plan) {
|
|
9
|
+
const createOpencodeSessionRegistry = () => ({ sessions: /* @__PURE__ */ new Map() });
|
|
10
|
+
const timeoutFailure = (milliseconds, message) => Effect.sleep(Duration.millis(milliseconds)).pipe(Effect.andThen(Effect.fail(new Error(message))));
|
|
11
|
+
const boundByAgentTimeout = (plan) => (effect) => {
|
|
12
|
+
const { timeoutMs } = plan;
|
|
13
|
+
if (timeoutMs === void 0 || timeoutMs <= 0) return effect;
|
|
14
|
+
return raceDetached(effect, timeoutFailure(timeoutMs, `agent session timed out after ${timeoutMs}ms`));
|
|
15
|
+
};
|
|
16
|
+
const validateOpencodePlan = (plan) => {
|
|
17
|
+
if (plan.type === "opencode") return Effect.void;
|
|
18
|
+
return Effect.fail(/* @__PURE__ */ new Error(`opencode executor cannot drive runner type '${plan.type}'`));
|
|
19
|
+
};
|
|
20
|
+
const sessionDirectory = (deps, plan) => plan.cwd;
|
|
21
|
+
const abortBestEffort = (deps, plan) => {
|
|
67
22
|
const sessionId = deps.registry.sessions.get(plan.nodeId);
|
|
68
|
-
if (
|
|
69
|
-
return Effect.gen(function* () {
|
|
23
|
+
if (sessionId === void 0) return Effect.void;
|
|
24
|
+
return Effect.gen(function* effectBody() {
|
|
70
25
|
yield* (yield* OpencodeSdkService).abortSession(deps.client, {
|
|
71
26
|
directory: sessionDirectory(deps, plan),
|
|
72
27
|
sessionID: sessionId
|
|
73
28
|
});
|
|
74
29
|
}).pipe(Effect.timeout(Duration.millis(5e3)), Effect.asVoid, Effect.catch(() => Effect.void));
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return plan.cwd ?? deps.directory;
|
|
82
|
-
}
|
|
83
|
-
function driveSession(deps, plan, options, activity) {
|
|
84
|
-
return Effect.gen(function* () {
|
|
85
|
-
const sessionId = yield* resolveSessionId(deps, plan, options);
|
|
86
|
-
recordSession(deps, plan.nodeId, sessionId);
|
|
87
|
-
const stream = yield* streamEventsToOutput(deps, sessionId, plan, options, activity);
|
|
88
|
-
return yield* promptSessionResult(deps, plan, sessionId, options).pipe(Effect.ensuring(stopStream(stream)));
|
|
30
|
+
};
|
|
31
|
+
const onIdleTimeout = (deps, plan, options, idleMs) => Effect.sync(() => {
|
|
32
|
+
options.onOutput?.({
|
|
33
|
+
chunk: `opencode session idle for ${idleMs}ms (no progress); aborting\n`,
|
|
34
|
+
nodeId: plan.nodeId,
|
|
35
|
+
stream: "stderr"
|
|
89
36
|
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
return
|
|
95
|
-
|
|
96
|
-
parts: data.parts ?? [],
|
|
97
|
-
sessionId
|
|
98
|
-
};
|
|
99
|
-
}), {
|
|
100
|
-
label: "session.prompt",
|
|
101
|
-
options,
|
|
102
|
-
plan
|
|
37
|
+
}).pipe(Effect.andThen(abortBestEffort(deps, plan)), Effect.andThen(Effect.fail(/* @__PURE__ */ new Error(`agent session idle for ${idleMs}ms (no progress)`))));
|
|
38
|
+
const idleWatchdog = (deps, plan, options, activity, idleMs) => {
|
|
39
|
+
const loop = Effect.suspend(() => {
|
|
40
|
+
const remaining = idleMs - (Date.now() - activity.last);
|
|
41
|
+
if (remaining <= 0) return onIdleTimeout(deps, plan, options, idleMs);
|
|
42
|
+
return Effect.sleep(Duration.millis(remaining)).pipe(Effect.andThen(loop));
|
|
103
43
|
});
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
|
|
44
|
+
return loop;
|
|
45
|
+
};
|
|
46
|
+
const boundByIdle = (deps, plan, options, activity) => (effect) => {
|
|
47
|
+
const idleMs = plan.idleTimeoutMs;
|
|
48
|
+
if (idleMs === void 0 || idleMs <= 0 || options.onOutput === void 0) return effect;
|
|
49
|
+
if (plan.timeoutMs !== void 0 && idleMs >= plan.timeoutMs) return effect;
|
|
50
|
+
return raceDetached(effect, idleWatchdog(deps, plan, options, activity, idleMs));
|
|
51
|
+
};
|
|
52
|
+
const stopStream = (stream) => Effect.tryPromise(async () => {
|
|
53
|
+
await stream.stop();
|
|
54
|
+
}).pipe(Effect.asVoid, Effect.catch(() => Effect.void));
|
|
55
|
+
const recordSession = (deps, nodeId, sessionId) => {
|
|
116
56
|
deps.onSession?.(nodeId, sessionId);
|
|
117
|
-
}
|
|
118
|
-
function resolveSessionId(deps, plan, options) {
|
|
119
|
-
const existing = deps.registry.sessions.get(plan.nodeId);
|
|
120
|
-
if (existing) return Effect.succeed(existing);
|
|
121
|
-
return Effect.gen(function* () {
|
|
122
|
-
const session = yield* retryTransientTransport(() => Effect.gen(function* () {
|
|
123
|
-
return yield* unwrapEffect(yield* (yield* OpencodeSdkService).createSession(deps.client, {
|
|
124
|
-
directory: plan.cwd ?? deps.directory,
|
|
125
|
-
title: `moka:${plan.nodeId}`
|
|
126
|
-
}));
|
|
127
|
-
}), {
|
|
128
|
-
label: "session.create",
|
|
129
|
-
options,
|
|
130
|
-
plan
|
|
131
|
-
});
|
|
132
|
-
deps.registry.sessions.set(plan.nodeId, session.id);
|
|
133
|
-
return session.id;
|
|
134
|
-
});
|
|
135
|
-
}
|
|
57
|
+
};
|
|
136
58
|
/**
|
|
137
59
|
* Bounded retry for transient OpenCode transport failures at the SDK boundary.
|
|
138
60
|
* Distinct from the node-level retry in retry.ts (which reprompts on gate
|
|
@@ -143,182 +65,133 @@ function resolveSessionId(deps, plan, options) {
|
|
|
143
65
|
*/
|
|
144
66
|
const MAX_TRANSIENT_RETRIES = 2;
|
|
145
67
|
const TRANSIENT_RETRY_BASE_MS = 250;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
function scheduleTransientRetry(make, ctx, attempt, error) {
|
|
150
|
-
const nextAttempt = attempt + 1;
|
|
151
|
-
const delay = Duration.millis(TRANSIENT_RETRY_BASE_MS * 2 ** attempt);
|
|
152
|
-
return emitTransientRetry(ctx, error, nextAttempt, delay).pipe(Effect.andThen(Effect.sleep(delay)), Effect.andThen(retryTransientTransport(make, ctx, nextAttempt)));
|
|
153
|
-
}
|
|
154
|
-
function emitTransientRetry(ctx, error, attempt, delay) {
|
|
155
|
-
return Effect.sync(() => {
|
|
156
|
-
ctx.options.onOutput?.({
|
|
157
|
-
chunk: `opencode ${ctx.label} transient failure: ${errorMessage(error)}; retry ${attempt}/${MAX_TRANSIENT_RETRIES} in ${Duration.toMillis(delay)}ms\n`,
|
|
158
|
-
nodeId: ctx.plan.nodeId,
|
|
159
|
-
stream: "stderr"
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
const TRANSIENT_TRANSPORT_RE = /fetch failed|econnreset|etimedout|enotfound|eai_again|socket hang ?up|network|connection (?:reset|closed|refused)|aborterror|operation was aborted|timed? ?out/i;
|
|
164
|
-
/**
|
|
165
|
-
* Retry only failures that prove the turn was NOT accepted: transport errors
|
|
166
|
-
* (no completed round trip) and HTTP 429/5xx rejections. Deterministic agent
|
|
167
|
-
* outcomes (output-length, aborted message, schema/contract problems) and gate
|
|
168
|
-
* failures are out of scope and never reach this classifier as retryable.
|
|
169
|
-
*/
|
|
170
|
-
function isTransientTransportError(error) {
|
|
171
|
-
if (TRANSIENT_TRANSPORT_RE.test(errorMessage(error))) return true;
|
|
172
|
-
const status = httpStatusFromError(error);
|
|
173
|
-
return status !== void 0 && (status === 429 || status >= 500);
|
|
174
|
-
}
|
|
175
|
-
function numericField(container, key) {
|
|
68
|
+
const TRANSIENT_TRANSPORT_RE = /fetch failed|econnreset|etimedout|enotfound|eai_again|socket hang ?up|network|connection (?:reset|closed|refused)|aborterror|operation was aborted|timed? ?out/iu;
|
|
69
|
+
const numericField = (container, key) => {
|
|
176
70
|
const value = isRecord(container) ? container[key] : void 0;
|
|
177
|
-
return
|
|
178
|
-
}
|
|
179
|
-
|
|
71
|
+
return isNumberValue(value) ? Option.some(value) : Option.none();
|
|
72
|
+
};
|
|
73
|
+
const httpStatusFromError = (error) => {
|
|
180
74
|
const response = isRecord(error) ? error.response : void 0;
|
|
181
|
-
return numericField(error, "status")
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return
|
|
185
|
-
|
|
186
|
-
|
|
75
|
+
return Option.orElse(numericField(error, "status"), () => Option.orElse(numericField(error, "statusCode"), () => numericField(response, "status")));
|
|
76
|
+
};
|
|
77
|
+
const parseModel = (model) => {
|
|
78
|
+
if (model === void 0 || model.length === 0) return Option.none();
|
|
79
|
+
const slash = model.indexOf("/");
|
|
80
|
+
if (slash === -1) return Option.none();
|
|
81
|
+
return Option.some({
|
|
82
|
+
modelID: model.slice(slash + 1),
|
|
83
|
+
providerID: model.slice(0, slash)
|
|
187
84
|
});
|
|
188
|
-
}
|
|
189
|
-
|
|
85
|
+
};
|
|
86
|
+
const promptBody = (plan) => {
|
|
190
87
|
const prompt = promptText(plan);
|
|
191
88
|
const model = parseModel(plan.model);
|
|
192
|
-
const agent = plan.profileId ? opencodeAgentName(plan.profileId)
|
|
89
|
+
const agent = plan.profileId === void 0 || plan.profileId.length === 0 ? Option.none() : Option.some(opencodeAgentName(plan.profileId));
|
|
193
90
|
return {
|
|
194
91
|
parts: [{
|
|
195
92
|
text: prompt,
|
|
196
93
|
type: "text"
|
|
197
94
|
}],
|
|
198
|
-
...agent
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
"--format",
|
|
208
|
-
"--model",
|
|
209
|
-
"--variant"
|
|
210
|
-
]);
|
|
211
|
-
/**
|
|
212
|
-
* The launch plan carries the prompt inside the CLI argv (`run <prompt>` or
|
|
213
|
-
* `run <prompt> --file <ctx>`). Recover it as the trailing positional arg
|
|
214
|
-
* (skipping flags and their values) so the adapter boundary is identical
|
|
215
|
-
* regardless of transport.
|
|
216
|
-
*/
|
|
217
|
-
function promptText(plan) {
|
|
218
|
-
return plan.args.filter((arg, index) => index > 0 && !arg.startsWith("-") && !FLAGS_TAKING_VALUE.has(plan.args[index - 1] ?? "")).at(-1) ?? "";
|
|
219
|
-
}
|
|
220
|
-
function parseModel(model) {
|
|
221
|
-
if (!model) return;
|
|
222
|
-
const slash = model.indexOf("/");
|
|
223
|
-
if (slash === -1) return;
|
|
224
|
-
return {
|
|
225
|
-
modelID: model.slice(slash + 1),
|
|
226
|
-
providerID: model.slice(0, slash)
|
|
95
|
+
...Option.match(agent, {
|
|
96
|
+
onNone: () => ({}),
|
|
97
|
+
onSome: (value) => ({ agent: value })
|
|
98
|
+
}),
|
|
99
|
+
...Option.match(model, {
|
|
100
|
+
onNone: () => ({}),
|
|
101
|
+
onSome: (value) => ({ model: value })
|
|
102
|
+
}),
|
|
103
|
+
...plan.variant === void 0 || plan.variant.length === 0 ? {} : { variant: plan.variant }
|
|
227
104
|
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
let done = false;
|
|
241
|
-
while (!done) {
|
|
242
|
-
const next = yield* readNextEvent(iterator);
|
|
243
|
-
done = next.done === true;
|
|
244
|
-
if (!done) {
|
|
245
|
-
activity.last = Date.now();
|
|
246
|
-
forwardEvent(next.value, sessionId, plan, options);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}).pipe(Effect.catch((error) => reportStreamDrop(error, plan, options)));
|
|
250
|
-
}
|
|
251
|
-
function readNextEvent(iterator) {
|
|
252
|
-
return Effect.tryPromise({
|
|
253
|
-
catch: (error) => error,
|
|
254
|
-
try: () => iterator.next()
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
function reportStreamDrop(error, plan, options) {
|
|
258
|
-
return Effect.sync(() => {
|
|
259
|
-
options.onOutput?.({
|
|
260
|
-
chunk: `opencode event stream dropped: ${errorMessage(error)}\n`,
|
|
261
|
-
nodeId: plan.nodeId,
|
|
262
|
-
stream: "stderr"
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
function stopIterator(iterator, pump) {
|
|
267
|
-
return Effect.runPromise(stopIteratorEffect(iterator, pump));
|
|
268
|
-
}
|
|
269
|
-
function stopIteratorEffect(iterator, pump) {
|
|
270
|
-
return Effect.gen(function* () {
|
|
271
|
-
yield* requestIteratorReturn(iterator);
|
|
272
|
-
yield* Effect.tryPromise(() => pump).pipe(Effect.catch(() => Effect.void));
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
function requestIteratorReturn(iterator) {
|
|
276
|
-
const returnIterator = iterator.return;
|
|
277
|
-
if (!returnIterator) return Effect.void;
|
|
105
|
+
};
|
|
106
|
+
const promptRequest = (deps, plan, sessionId) => ({
|
|
107
|
+
directory: sessionDirectory(deps, plan),
|
|
108
|
+
sessionID: sessionId,
|
|
109
|
+
...promptBody(plan)
|
|
110
|
+
});
|
|
111
|
+
const readNextEvent = (iterator) => Effect.tryPromise({
|
|
112
|
+
catch: (error) => error,
|
|
113
|
+
try: async () => await iterator.next()
|
|
114
|
+
});
|
|
115
|
+
const requestIteratorReturn = (iterator) => {
|
|
116
|
+
if (iterator.return === void 0) return Effect.void;
|
|
278
117
|
return Effect.tryPromise({
|
|
279
118
|
catch: (error) => error,
|
|
280
|
-
try: () =>
|
|
119
|
+
try: async () => {
|
|
120
|
+
await iterator.return?.();
|
|
121
|
+
}
|
|
281
122
|
}).pipe(Effect.asVoid, Effect.catch(() => Effect.void));
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
function belongsToSession(event, sessionId) {
|
|
299
|
-
return event.properties.sessionID === void 0 || event.properties.sessionID === sessionId;
|
|
300
|
-
}
|
|
301
|
-
function partChunk(part, sessionId) {
|
|
302
|
-
if (part.sessionID !== sessionId) return;
|
|
303
|
-
if (part.type === "text") return {
|
|
123
|
+
};
|
|
124
|
+
const stopIteratorEffect = (iterator, pump) => Effect.gen(function* effectBody() {
|
|
125
|
+
yield* requestIteratorReturn(iterator);
|
|
126
|
+
yield* Effect.tryPromise(async () => {
|
|
127
|
+
await pump;
|
|
128
|
+
}).pipe(Effect.catch(() => Effect.void));
|
|
129
|
+
});
|
|
130
|
+
const stopIterator = async (iterator, pump) => {
|
|
131
|
+
await Effect.runPromise(stopIteratorEffect(iterator, pump));
|
|
132
|
+
};
|
|
133
|
+
const belongsToSession = (event, sessionId) => event.properties.sessionID === void 0 || event.properties.sessionID === sessionId;
|
|
134
|
+
const partChunk = (part, sessionId) => {
|
|
135
|
+
if (part.sessionID !== sessionId) return Option.none();
|
|
136
|
+
if (part.type === "text") return Option.some({
|
|
304
137
|
chunk: `${JSON.stringify({ part: {
|
|
305
138
|
text: part.text,
|
|
306
139
|
type: "text"
|
|
307
140
|
} })}\n`,
|
|
308
141
|
stream: "stdout"
|
|
309
|
-
};
|
|
310
|
-
if (part.type === "tool") return {
|
|
142
|
+
});
|
|
143
|
+
if (part.type === "tool") return Option.some({
|
|
311
144
|
chunk: `opencode tool ${part.tool} ${part.state.status}\n`,
|
|
312
145
|
stream: "stderr"
|
|
313
|
-
};
|
|
314
|
-
|
|
146
|
+
});
|
|
147
|
+
return Option.none();
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Map opencode message errors to the runner's infra-vs-agent exit convention.
|
|
151
|
+
* Output-length / aborted are agent-task outcomes (exit 1, gate territory);
|
|
152
|
+
* provider-auth, API, and unknown are infra (exit 70, retry-eligible).
|
|
153
|
+
*/
|
|
154
|
+
const infraErrorExitCode = (error) => {
|
|
155
|
+
switch (error.name) {
|
|
156
|
+
case "MessageOutputLengthError":
|
|
157
|
+
case "MessageAbortedError": return 1;
|
|
158
|
+
case "ContentFilterError":
|
|
159
|
+
case "ContextOverflowError":
|
|
160
|
+
case "StructuredOutputError": return 1;
|
|
161
|
+
case "APIError":
|
|
162
|
+
case "ProviderAuthError":
|
|
163
|
+
case "UnknownError": return 70;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
const describeMessageError = (error) => {
|
|
167
|
+
if (error === void 0) return "unknown opencode error";
|
|
168
|
+
const data = isRecord(error.data) ? error.data : void 0;
|
|
169
|
+
const detail = data !== void 0 && isStringValue(data.message) ? `: ${data.message}` : "";
|
|
170
|
+
return `${error.name}${detail}`;
|
|
171
|
+
};
|
|
172
|
+
const eventChunk = (event, sessionId) => {
|
|
173
|
+
if (event.type === "message.part.updated") return partChunk(event.properties.part, sessionId);
|
|
174
|
+
if (event.type === "session.error" && belongsToSession(event, sessionId)) return Option.some({
|
|
175
|
+
chunk: `opencode session error: ${describeMessageError(event.properties.error)}\n`,
|
|
176
|
+
stream: "stderr"
|
|
177
|
+
});
|
|
178
|
+
return Option.none();
|
|
179
|
+
};
|
|
180
|
+
const forwardEvent = (event, sessionId, plan, options) => {
|
|
181
|
+
const forwarded = eventChunk(event, sessionId);
|
|
182
|
+
if (Option.isSome(forwarded)) options.onOutput?.({
|
|
183
|
+
chunk: forwarded.value.chunk,
|
|
184
|
+
nodeId: plan.nodeId,
|
|
185
|
+
stream: forwarded.value.stream
|
|
186
|
+
});
|
|
187
|
+
};
|
|
315
188
|
/**
|
|
316
189
|
* Reconstruct the JSONL stdout the existing normalizeOutput/outputCandidates
|
|
317
190
|
* parser already understands, so the structured-output and repair passes work
|
|
318
191
|
* unchanged on top of SDK responses.
|
|
319
192
|
*/
|
|
320
|
-
|
|
321
|
-
const stdout = drive.parts.filter((part) => part.type === "text").map((part) => JSON.stringify({ part: {
|
|
193
|
+
const successResult = (plan, drive) => {
|
|
194
|
+
const stdout = drive.parts.filter((part) => part.type === "text" && isStringValue(part.text)).map((part) => JSON.stringify({ part: {
|
|
322
195
|
text: part.text,
|
|
323
196
|
type: "text"
|
|
324
197
|
} })).join("\n");
|
|
@@ -336,38 +209,40 @@ function successResult(plan, drive) {
|
|
|
336
209
|
sessionId: drive.sessionId,
|
|
337
210
|
stdout
|
|
338
211
|
};
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
return
|
|
370
|
-
|
|
212
|
+
};
|
|
213
|
+
const nonEmptyString = (error) => isStringValue(error) && error.length > 0 ? Option.some(error) : Option.none();
|
|
214
|
+
const nonEmptyJson = (error) => {
|
|
215
|
+
if (error === void 0) return Option.none();
|
|
216
|
+
const json = JSON.stringify(error);
|
|
217
|
+
return json.length > 0 && json !== "{}" ? Option.some(json) : Option.none();
|
|
218
|
+
};
|
|
219
|
+
const requestTarget = (request) => `${request?.method ?? "?"} ${request?.url ?? "?"}`;
|
|
220
|
+
const hasRequestTarget = (request) => request?.method !== void 0 && request.method.length > 0 || request?.url !== void 0 && request.url.length > 0;
|
|
221
|
+
const isHttpStatus = (status) => status !== void 0 && status !== 0 && !Number.isNaN(status);
|
|
222
|
+
const statusSuffix = (status) => isHttpStatus(status) ? ` → HTTP ${status}` : "";
|
|
223
|
+
const httpStatusLine = (result) => {
|
|
224
|
+
const status = result.response?.status;
|
|
225
|
+
const { request } = result;
|
|
226
|
+
if (!(hasRequestTarget(request) || isHttpStatus(status))) return Option.none();
|
|
227
|
+
return Option.some(`${requestTarget(request)}${statusSuffix(status)}`);
|
|
228
|
+
};
|
|
229
|
+
const httpContext = (result) => {
|
|
230
|
+
const status = httpStatusLine(result);
|
|
231
|
+
return Option.match(status, {
|
|
232
|
+
onNone: () => "",
|
|
233
|
+
onSome: (value) => ` (${value})`
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
const stringField = (value, field) => {
|
|
237
|
+
const fieldValue = isRecord(value) ? value[field] : void 0;
|
|
238
|
+
if (isStringValue(fieldValue)) return fieldValue.length > 0 ? Option.some(fieldValue) : Option.none();
|
|
239
|
+
return Option.none();
|
|
240
|
+
};
|
|
241
|
+
const bodyMessage = (error) => {
|
|
242
|
+
if (!isRecord(error)) return Option.none();
|
|
243
|
+
return Option.orElse(stringField(error.data, "message"), () => Option.orElse(stringField(error, "message"), () => stringField(error, "name")));
|
|
244
|
+
};
|
|
245
|
+
const errorDetail = (error) => Option.orElse(bodyMessage(error), () => Option.orElse(nonEmptyString(error), () => nonEmptyJson(error)));
|
|
371
246
|
/**
|
|
372
247
|
* Build the richest available message for a failed opencode result tuple. The
|
|
373
248
|
* runner reads the result-tuple path (not throwOnError), so the SDK leaves
|
|
@@ -377,47 +252,140 @@ function unwrap(result) {
|
|
|
377
252
|
* surfaces as "POST …/prompt → 504 Gateway Timeout" instead of "{}". Mirrors the
|
|
378
253
|
* SDK's own error-interceptor describe().
|
|
379
254
|
*/
|
|
380
|
-
|
|
255
|
+
const resultErrorMessage = (result) => {
|
|
381
256
|
const detail = errorDetail(result.error);
|
|
382
|
-
if (detail) return `${detail}${httpContext(result)}`;
|
|
383
|
-
return httpStatusLine(result)
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
return
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
416
|
-
function
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
257
|
+
if (Option.isSome(detail)) return `${detail.value}${httpContext(result)}`;
|
|
258
|
+
return Option.getOrElse(httpStatusLine(result), () => "opencode error with empty response body");
|
|
259
|
+
};
|
|
260
|
+
const unwrap = (result) => {
|
|
261
|
+
if (result.error !== void 0) throw new Error(resultErrorMessage(result));
|
|
262
|
+
if (result.data === void 0) throw new Error(`opencode response contained no data${httpContext(result)}`);
|
|
263
|
+
return result.data;
|
|
264
|
+
};
|
|
265
|
+
const unwrapEffect = (response) => Effect.try({
|
|
266
|
+
catch: (error) => error,
|
|
267
|
+
try: () => unwrap(response)
|
|
268
|
+
});
|
|
269
|
+
const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
270
|
+
const emitTransientRetry = (ctx, error, attempt, delay) => Effect.sync(() => {
|
|
271
|
+
ctx.options.onOutput?.({
|
|
272
|
+
chunk: `opencode ${ctx.label} transient failure: ${errorMessage(error)}; retry ${attempt}/${MAX_TRANSIENT_RETRIES} in ${Duration.toMillis(delay)}ms\n`,
|
|
273
|
+
nodeId: ctx.plan.nodeId,
|
|
274
|
+
stream: "stderr"
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
/**
|
|
278
|
+
* Retry only failures that prove the turn was NOT accepted: transport errors
|
|
279
|
+
* (no completed round trip) and HTTP 429/5xx rejections. Deterministic agent
|
|
280
|
+
* outcomes (output-length, aborted message, schema/contract problems) and gate
|
|
281
|
+
* failures are out of scope and never reach this classifier as retryable.
|
|
282
|
+
*/
|
|
283
|
+
const isTransientTransportError = (error) => {
|
|
284
|
+
if (TRANSIENT_TRANSPORT_RE.test(errorMessage(error))) return true;
|
|
285
|
+
const status = httpStatusFromError(error);
|
|
286
|
+
return Option.match(status, {
|
|
287
|
+
onNone: () => false,
|
|
288
|
+
onSome: (value) => value === 429 || value >= 500
|
|
289
|
+
});
|
|
290
|
+
};
|
|
291
|
+
const retryTransientTransport = function retryTransientTransport(make, ctx, attempt = 0) {
|
|
292
|
+
return make().pipe(Effect.catch((error) => {
|
|
293
|
+
if (!(attempt < MAX_TRANSIENT_RETRIES && isTransientTransportError(error))) return Effect.fail(error);
|
|
294
|
+
const nextAttempt = attempt + 1;
|
|
295
|
+
const delay = Duration.millis(TRANSIENT_RETRY_BASE_MS * 2 ** attempt);
|
|
296
|
+
return emitTransientRetry(ctx, error, nextAttempt, delay).pipe(Effect.andThen(Effect.sleep(delay)), Effect.andThen(retryTransientTransport(make, ctx, nextAttempt)));
|
|
297
|
+
}));
|
|
298
|
+
};
|
|
299
|
+
const promptSessionResult = (deps, plan, sessionId, options) => retryTransientTransport(() => Effect.gen(function* effectBody() {
|
|
300
|
+
const response = yield* (yield* OpencodeSdkService).promptSession(deps.client, promptRequest(deps, plan, sessionId));
|
|
301
|
+
const data = yield* unwrapEffect(response);
|
|
302
|
+
return {
|
|
303
|
+
assistant: data.info,
|
|
304
|
+
parts: data.parts,
|
|
305
|
+
sessionId
|
|
306
|
+
};
|
|
307
|
+
}), {
|
|
308
|
+
label: "session.prompt",
|
|
309
|
+
options,
|
|
310
|
+
plan
|
|
311
|
+
});
|
|
312
|
+
const resolveSessionId = (deps, plan, options) => {
|
|
313
|
+
const existing = deps.registry.sessions.get(plan.nodeId);
|
|
314
|
+
if (existing !== void 0 && existing.length > 0) return Effect.succeed(existing);
|
|
315
|
+
return Effect.gen(function* effectBody() {
|
|
316
|
+
const session = yield* retryTransientTransport(() => Effect.gen(function* session() {
|
|
317
|
+
const created = yield* (yield* OpencodeSdkService).createSession(deps.client, {
|
|
318
|
+
directory: plan.cwd,
|
|
319
|
+
title: `moka:${plan.nodeId}`
|
|
320
|
+
});
|
|
321
|
+
return yield* unwrapEffect(created);
|
|
322
|
+
}), {
|
|
323
|
+
label: "session.create",
|
|
324
|
+
options,
|
|
325
|
+
plan
|
|
326
|
+
});
|
|
327
|
+
deps.registry.sessions.set(plan.nodeId, session.id);
|
|
328
|
+
return session.id;
|
|
329
|
+
});
|
|
330
|
+
};
|
|
331
|
+
const reportStreamDrop = (error, plan, options) => Effect.sync(() => {
|
|
332
|
+
options.onOutput?.({
|
|
333
|
+
chunk: `opencode event stream dropped: ${errorMessage(error)}\n`,
|
|
334
|
+
nodeId: plan.nodeId,
|
|
335
|
+
stream: "stderr"
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
const pumpEvents = (iterator, sessionId, plan, options, activity) => Effect.gen(function* effectBody() {
|
|
339
|
+
let done = false;
|
|
340
|
+
while (!done) {
|
|
341
|
+
const next = yield* readNextEvent(iterator);
|
|
342
|
+
done = next.done === true;
|
|
343
|
+
if (!done) {
|
|
344
|
+
activity.last = Date.now();
|
|
345
|
+
forwardEvent(next.value, sessionId, plan, options);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}).pipe(Effect.catch((error) => reportStreamDrop(error, plan, options)));
|
|
349
|
+
const streamEventsToOutput = (deps, sessionId, plan, options, activity) => {
|
|
350
|
+
if (options.onOutput === void 0) return Effect.succeed({ stop: async () => {} });
|
|
351
|
+
return Effect.gen(function* effectBody() {
|
|
352
|
+
const iterator = (yield* (yield* OpencodeSdkService).subscribeEvents(deps.client)).stream;
|
|
353
|
+
activity.last = Date.now();
|
|
354
|
+
const pump = Effect.runPromise(pumpEvents(iterator, sessionId, plan, options, activity));
|
|
355
|
+
return { stop: async () => {
|
|
356
|
+
await stopIterator(iterator, pump);
|
|
357
|
+
} };
|
|
358
|
+
});
|
|
359
|
+
};
|
|
360
|
+
const driveSession = (deps, plan, options, activity) => Effect.gen(function* effectBody() {
|
|
361
|
+
const sessionId = yield* resolveSessionId(deps, plan, options);
|
|
362
|
+
recordSession(deps, plan.nodeId, sessionId);
|
|
363
|
+
const stream = yield* streamEventsToOutput(deps, sessionId, plan, options, activity);
|
|
364
|
+
return yield* promptSessionResult(deps, plan, sessionId, options).pipe(Effect.ensuring(stopStream(stream)));
|
|
365
|
+
});
|
|
366
|
+
const failureResult = (plan, error) => ({
|
|
367
|
+
argv: plan.args,
|
|
368
|
+
exitCode: 70,
|
|
369
|
+
stderr: `opencode session failed: ${errorMessage(error)}`,
|
|
370
|
+
stdout: ""
|
|
371
|
+
});
|
|
372
|
+
const executeOpencodeSession = (deps, plan, options) => {
|
|
373
|
+
const activity = { last: Date.now() };
|
|
374
|
+
return Effect.gen(function* effectBody() {
|
|
375
|
+
const drive = yield* driveSession(deps, plan, options, activity);
|
|
376
|
+
return successResult(plan, drive);
|
|
377
|
+
}).pipe(boundByIdle(deps, plan, options, activity), boundByAgentTimeout(plan), Effect.catch((error) => Effect.succeed(failureResult(plan, error))));
|
|
378
|
+
};
|
|
379
|
+
const executeOpencodeEffect = (deps, plan, options) => Effect.gen(function* effectBody() {
|
|
380
|
+
yield* validateOpencodePlan(plan);
|
|
381
|
+
return yield* executeOpencodeSession(deps, plan, options);
|
|
382
|
+
});
|
|
383
|
+
/**
|
|
384
|
+
* SDK-backed replacement for the subprocess `runLaunchPlan`. Conforms to the
|
|
385
|
+
* RuntimeContext.executor seam so agent-node never learns the transport.
|
|
386
|
+
*/
|
|
387
|
+
const createOpencodeExecutor = (deps) => async function execute(plan, options = {}) {
|
|
388
|
+
return await Effect.runPromise(Effect.provide(executeOpencodeEffect(deps, plan, options), OpencodeSdkServiceLive), options.signal ? { signal: options.signal } : void 0);
|
|
389
|
+
};
|
|
422
390
|
//#endregion
|
|
423
391
|
export { createOpencodeExecutor, createOpencodeSessionRegistry };
|