@loompad/cli 0.1.0 → 1.1.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/ARCHITECTURE.md +96 -11
- package/CHANGELOG.md +1042 -0
- package/README.md +1473 -44
- package/THIRD_PARTY_NOTICES.md +275 -0
- package/dist/adapters/antigravity-cli.d.ts +62 -0
- package/dist/adapters/antigravity-cli.js +258 -0
- package/dist/adapters/antigravity-cli.js.map +1 -0
- package/dist/adapters/base.d.ts +61 -9
- package/dist/adapters/base.js +266 -15
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/bridges/antigravity.d.ts +9 -12
- package/dist/adapters/bridges/antigravity.js +9 -12
- package/dist/adapters/bridges/antigravity.js.map +1 -1
- package/dist/adapters/echo.d.ts +1 -0
- package/dist/adapters/echo.js +59 -3
- package/dist/adapters/echo.js.map +1 -1
- package/dist/adapters/grok.d.ts +7 -0
- package/dist/adapters/grok.js +20 -10
- package/dist/adapters/grok.js.map +1 -1
- package/dist/adapters/index.d.ts +5 -1
- package/dist/adapters/index.js +53 -7
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/model.d.ts +137 -0
- package/dist/adapters/model.js +527 -0
- package/dist/adapters/model.js.map +1 -0
- package/dist/adapters/opencode.d.ts +101 -1
- package/dist/adapters/opencode.js +874 -98
- package/dist/adapters/opencode.js.map +1 -1
- package/dist/cli/doctor.d.ts +32 -0
- package/dist/cli/doctor.js +89 -4
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.js +2651 -87
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/tui-model.js +3 -1
- package/dist/cli/tui-model.js.map +1 -1
- package/dist/cli/tui.js +44 -1
- package/dist/cli/tui.js.map +1 -1
- package/dist/cli/ui.d.ts +2 -0
- package/dist/cli/ui.js +13 -0
- package/dist/cli/ui.js.map +1 -1
- package/dist/core/ades.d.ts +32 -16
- package/dist/core/ades.js +57 -20
- package/dist/core/ades.js.map +1 -1
- package/dist/core/agent-state.d.ts +14 -0
- package/dist/core/agent-state.js +32 -0
- package/dist/core/agent-state.js.map +1 -0
- package/dist/core/approvals.d.ts +33 -0
- package/dist/core/approvals.js +29 -0
- package/dist/core/approvals.js.map +1 -0
- package/dist/core/backup.d.ts +19 -0
- package/dist/core/backup.js +86 -0
- package/dist/core/backup.js.map +1 -0
- package/dist/core/baton.d.ts +2 -2
- package/dist/core/baton.js.map +1 -1
- package/dist/core/brain-extract.d.ts +17 -0
- package/dist/core/brain-extract.js +28 -2
- package/dist/core/brain-extract.js.map +1 -1
- package/dist/core/brain-index.d.ts +71 -0
- package/dist/core/brain-index.js +204 -5
- package/dist/core/brain-index.js.map +1 -1
- package/dist/core/brain.d.ts +58 -2
- package/dist/core/brain.js +79 -1
- package/dist/core/brain.js.map +1 -1
- package/dist/core/chat-import.d.ts +71 -0
- package/dist/core/chat-import.js +419 -0
- package/dist/core/chat-import.js.map +1 -0
- package/dist/core/checkpoint.d.ts +107 -0
- package/dist/core/checkpoint.js +259 -0
- package/dist/core/checkpoint.js.map +1 -0
- package/dist/core/claude-cli.js +1 -1
- package/dist/core/claude-cli.js.map +1 -1
- package/dist/core/continuity/artifacts.d.ts +14 -0
- package/dist/core/continuity/artifacts.js +118 -0
- package/dist/core/continuity/artifacts.js.map +1 -0
- package/dist/core/continuity/capabilities.d.ts +65 -0
- package/dist/core/continuity/capabilities.js +129 -0
- package/dist/core/continuity/capabilities.js.map +1 -0
- package/dist/core/continuity/contracts.d.ts +352 -0
- package/dist/core/continuity/contracts.js +117 -0
- package/dist/core/continuity/contracts.js.map +1 -0
- package/dist/core/continuity/engine.d.ts +55 -0
- package/dist/core/continuity/engine.js +504 -0
- package/dist/core/continuity/engine.js.map +1 -0
- package/dist/core/continuity/store.d.ts +73 -0
- package/dist/core/continuity/store.js +438 -0
- package/dist/core/continuity/store.js.map +1 -0
- package/dist/core/conversations.d.ts +19 -0
- package/dist/core/conversations.js +87 -0
- package/dist/core/conversations.js.map +1 -0
- package/dist/core/crew-coordination.d.ts +6 -0
- package/dist/core/crew-coordination.js +24 -0
- package/dist/core/crew-coordination.js.map +1 -0
- package/dist/core/crew-policy.d.ts +18 -0
- package/dist/core/crew-policy.js +40 -0
- package/dist/core/crew-policy.js.map +1 -0
- package/dist/core/crew-protocol.d.ts +64 -0
- package/dist/core/crew-protocol.js +146 -0
- package/dist/core/crew-protocol.js.map +1 -0
- package/dist/core/crew-requests.d.ts +14 -0
- package/dist/core/crew-requests.js +22 -0
- package/dist/core/crew-requests.js.map +1 -0
- package/dist/core/crew.d.ts +395 -0
- package/dist/core/crew.js +1857 -0
- package/dist/core/crew.js.map +1 -0
- package/dist/core/digest.d.ts +40 -0
- package/dist/core/digest.js +94 -0
- package/dist/core/digest.js.map +1 -0
- package/dist/core/distill.js +3 -5
- package/dist/core/distill.js.map +1 -1
- package/dist/core/eventlog.d.ts +27 -25
- package/dist/core/eventlog.js +97 -150
- package/dist/core/eventlog.js.map +1 -1
- package/dist/core/events/contracts.d.ts +51 -0
- package/dist/core/events/contracts.js +2 -0
- package/dist/core/events/contracts.js.map +1 -0
- package/dist/core/events/jsonl-store.d.ts +23 -0
- package/dist/core/events/jsonl-store.js +90 -0
- package/dist/core/events/jsonl-store.js.map +1 -0
- package/dist/core/events/sqlite-store.d.ts +37 -0
- package/dist/core/events/sqlite-store.js +215 -0
- package/dist/core/events/sqlite-store.js.map +1 -0
- package/dist/core/forecast.d.ts +44 -0
- package/dist/core/forecast.js +46 -0
- package/dist/core/forecast.js.map +1 -0
- package/dist/core/free-pool.d.ts +65 -0
- package/dist/core/free-pool.js +171 -0
- package/dist/core/free-pool.js.map +1 -0
- package/dist/core/git.d.ts +30 -0
- package/dist/core/git.js +50 -2
- package/dist/core/git.js.map +1 -1
- package/dist/core/github-events.d.ts +53 -0
- package/dist/core/github-events.js +227 -0
- package/dist/core/github-events.js.map +1 -0
- package/dist/core/goal-lanes.d.ts +53 -0
- package/dist/core/goal-lanes.js +99 -0
- package/dist/core/goal-lanes.js.map +1 -0
- package/dist/core/hosted.d.ts +27 -0
- package/dist/core/hosted.js +50 -0
- package/dist/core/hosted.js.map +1 -0
- package/dist/core/http-server.d.ts +5 -0
- package/dist/core/http-server.js +8 -0
- package/dist/core/http-server.js.map +1 -0
- package/dist/core/invite-link.d.ts +31 -0
- package/dist/core/invite-link.js +45 -0
- package/dist/core/invite-link.js.map +1 -0
- package/dist/core/loops.d.ts +37 -0
- package/dist/core/loops.js +81 -0
- package/dist/core/loops.js.map +1 -0
- package/dist/core/mcp-catalog.d.ts +182 -0
- package/dist/core/mcp-catalog.js +431 -0
- package/dist/core/mcp-catalog.js.map +1 -0
- package/dist/core/mcp.d.ts +87 -0
- package/dist/core/mcp.js +181 -0
- package/dist/core/mcp.js.map +1 -0
- package/dist/core/memory.d.ts +1 -0
- package/dist/core/memory.js +5 -2
- package/dist/core/memory.js.map +1 -1
- package/dist/core/model-tools.d.ts +97 -0
- package/dist/core/model-tools.js +388 -0
- package/dist/core/model-tools.js.map +1 -0
- package/dist/core/notify.js +19 -1
- package/dist/core/notify.js.map +1 -1
- package/dist/core/orchestra.d.ts +635 -0
- package/dist/core/orchestra.js +2120 -0
- package/dist/core/orchestra.js.map +1 -0
- package/dist/core/permissions.d.ts +56 -0
- package/dist/core/permissions.js +122 -0
- package/dist/core/permissions.js.map +1 -0
- package/dist/core/preview-proxy.d.ts +70 -0
- package/dist/core/preview-proxy.js +563 -0
- package/dist/core/preview-proxy.js.map +1 -0
- package/dist/core/preview-shot.d.ts +46 -0
- package/dist/core/preview-shot.js +109 -0
- package/dist/core/preview-shot.js.map +1 -0
- package/dist/core/process.d.ts +36 -0
- package/dist/core/process.js +168 -0
- package/dist/core/process.js.map +1 -0
- package/dist/core/prompt-queue.d.ts +136 -0
- package/dist/core/prompt-queue.js +269 -0
- package/dist/core/prompt-queue.js.map +1 -0
- package/dist/core/prompts.d.ts +53 -0
- package/dist/core/prompts.js +127 -0
- package/dist/core/prompts.js.map +1 -0
- package/dist/core/proof.d.ts +82 -0
- package/dist/core/proof.js +216 -0
- package/dist/core/proof.js.map +1 -0
- package/dist/core/provider-text.d.ts +23 -0
- package/dist/core/provider-text.js +66 -0
- package/dist/core/provider-text.js.map +1 -0
- package/dist/core/providers.d.ts +152 -0
- package/dist/core/providers.js +343 -0
- package/dist/core/providers.js.map +1 -0
- package/dist/core/quota.d.ts +33 -0
- package/dist/core/quota.js +74 -0
- package/dist/core/quota.js.map +1 -0
- package/dist/core/recipes.d.ts +50 -0
- package/dist/core/recipes.js +99 -0
- package/dist/core/recipes.js.map +1 -0
- package/dist/core/registry.d.ts +54 -0
- package/dist/core/registry.js +62 -5
- package/dist/core/registry.js.map +1 -1
- package/dist/core/relay-client.d.ts +67 -0
- package/dist/core/relay-client.js +184 -0
- package/dist/core/relay-client.js.map +1 -0
- package/dist/core/relay-protocol.d.ts +140 -0
- package/dist/core/relay-protocol.js +189 -0
- package/dist/core/relay-protocol.js.map +1 -0
- package/dist/core/replay.d.ts +18 -0
- package/dist/core/replay.js +191 -0
- package/dist/core/replay.js.map +1 -0
- package/dist/core/review-triage.d.ts +41 -0
- package/dist/core/review-triage.js +124 -0
- package/dist/core/review-triage.js.map +1 -0
- package/dist/core/router.js +2 -2
- package/dist/core/router.js.map +1 -1
- package/dist/core/routes.d.ts +39 -3
- package/dist/core/routes.js +137 -4
- package/dist/core/routes.js.map +1 -1
- package/dist/core/run-command.d.ts +14 -0
- package/dist/core/run-command.js +45 -0
- package/dist/core/run-command.js.map +1 -0
- package/dist/core/runner-setup.d.ts +36 -0
- package/dist/core/runner-setup.js +114 -0
- package/dist/core/runner-setup.js.map +1 -0
- package/dist/core/search.js +0 -0
- package/dist/core/search.js.map +1 -1
- package/dist/core/semantic.d.ts +92 -0
- package/dist/core/semantic.js +227 -0
- package/dist/core/semantic.js.map +1 -0
- package/dist/core/servers.d.ts +115 -0
- package/dist/core/servers.js +305 -0
- package/dist/core/servers.js.map +1 -0
- package/dist/core/setup.js +64 -6
- package/dist/core/setup.js.map +1 -1
- package/dist/core/shield.d.ts +17 -0
- package/dist/core/shield.js +81 -0
- package/dist/core/shield.js.map +1 -0
- package/dist/core/shift.d.ts +69 -0
- package/dist/core/shift.js +120 -0
- package/dist/core/shift.js.map +1 -0
- package/dist/core/skill-install.d.ts +125 -0
- package/dist/core/skill-install.js +328 -0
- package/dist/core/skill-install.js.map +1 -0
- package/dist/core/skills.d.ts +116 -0
- package/dist/core/skills.js +223 -0
- package/dist/core/skills.js.map +1 -0
- package/dist/core/step-conditions.d.ts +65 -0
- package/dist/core/step-conditions.js +102 -0
- package/dist/core/step-conditions.js.map +1 -0
- package/dist/core/subagents.d.ts +45 -0
- package/dist/core/subagents.js +90 -0
- package/dist/core/subagents.js.map +1 -0
- package/dist/core/team-canon.d.ts +42 -0
- package/dist/core/team-canon.js +121 -0
- package/dist/core/team-canon.js.map +1 -0
- package/dist/core/team-crypto.d.ts +97 -0
- package/dist/core/team-crypto.js +175 -0
- package/dist/core/team-crypto.js.map +1 -0
- package/dist/core/team-hub.d.ts +514 -0
- package/dist/core/team-hub.js +964 -0
- package/dist/core/team-hub.js.map +1 -0
- package/dist/core/team-landing.d.ts +239 -0
- package/dist/core/team-landing.js +504 -0
- package/dist/core/team-landing.js.map +1 -0
- package/dist/core/team-leases.d.ts +44 -0
- package/dist/core/team-leases.js +145 -0
- package/dist/core/team-leases.js.map +1 -0
- package/dist/core/team-memory.d.ts +42 -0
- package/dist/core/team-memory.js +84 -0
- package/dist/core/team-memory.js.map +1 -0
- package/dist/core/team-policy.d.ts +85 -0
- package/dist/core/team-policy.js +191 -0
- package/dist/core/team-policy.js.map +1 -0
- package/dist/core/thread-md.d.ts +6 -0
- package/dist/core/thread-md.js +42 -0
- package/dist/core/thread-md.js.map +1 -0
- package/dist/core/turn-stats.d.ts +61 -0
- package/dist/core/turn-stats.js +124 -0
- package/dist/core/turn-stats.js.map +1 -0
- package/dist/core/updater.d.ts +73 -0
- package/dist/core/updater.js +153 -0
- package/dist/core/updater.js.map +1 -0
- package/dist/core/usage.d.ts +75 -0
- package/dist/core/usage.js +163 -0
- package/dist/core/usage.js.map +1 -0
- package/dist/core/windows-jobs.d.ts +15 -0
- package/dist/core/windows-jobs.js +170 -0
- package/dist/core/windows-jobs.js.map +1 -0
- package/dist/core/worktree-merge.d.ts +57 -0
- package/dist/core/worktree-merge.js +123 -0
- package/dist/core/worktree-merge.js.map +1 -0
- package/dist/core/worktree.d.ts +5 -0
- package/dist/core/worktree.js +45 -5
- package/dist/core/worktree.js.map +1 -1
- package/dist/core/worktrees.d.ts +6 -0
- package/dist/core/worktrees.js +21 -0
- package/dist/core/worktrees.js.map +1 -0
- package/dist/daemon/app-page.d.ts +0 -17
- package/dist/daemon/app-page.js +10 -6346
- package/dist/daemon/app-page.js.map +1 -1
- package/dist/daemon/auth.d.ts +24 -3
- package/dist/daemon/auth.js +66 -7
- package/dist/daemon/auth.js.map +1 -1
- package/dist/daemon/board.d.ts +7 -0
- package/dist/daemon/board.js +12 -1
- package/dist/daemon/board.js.map +1 -1
- package/dist/daemon/brand-icons.d.ts +6 -1
- package/dist/daemon/brand-icons.js +11 -3
- package/dist/daemon/brand-icons.js.map +1 -1
- package/dist/daemon/client.d.ts +665 -1
- package/dist/daemon/client.js +381 -3
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/delivery.d.ts +34 -0
- package/dist/daemon/delivery.js +63 -0
- package/dist/daemon/delivery.js.map +1 -0
- package/dist/daemon/deploys.d.ts +62 -0
- package/dist/daemon/deploys.js +159 -0
- package/dist/daemon/deploys.js.map +1 -0
- package/dist/daemon/landing.d.ts +192 -0
- package/dist/daemon/landing.js +992 -0
- package/dist/daemon/landing.js.map +1 -0
- package/dist/daemon/onboard.d.ts +150 -0
- package/dist/daemon/onboard.js +390 -0
- package/dist/daemon/onboard.js.map +1 -0
- package/dist/daemon/proof.d.ts +70 -0
- package/dist/daemon/proof.js +172 -0
- package/dist/daemon/proof.js.map +1 -0
- package/dist/daemon/push.d.ts +19 -1
- package/dist/daemon/push.js +90 -3
- package/dist/daemon/push.js.map +1 -1
- package/dist/daemon/relay.d.ts +82 -0
- package/dist/daemon/relay.js +351 -0
- package/dist/daemon/relay.js.map +1 -0
- package/dist/daemon/routes/agent-discovery.d.ts +4 -0
- package/dist/daemon/routes/agent-discovery.js +29 -0
- package/dist/daemon/routes/agent-discovery.js.map +1 -0
- package/dist/daemon/routes/agent-settings.d.ts +4 -0
- package/dist/daemon/routes/agent-settings.js +94 -0
- package/dist/daemon/routes/agent-settings.js.map +1 -0
- package/dist/daemon/routes/agents.d.ts +4 -0
- package/dist/daemon/routes/agents.js +105 -0
- package/dist/daemon/routes/agents.js.map +1 -0
- package/dist/daemon/routes/alerts.d.ts +4 -0
- package/dist/daemon/routes/alerts.js +137 -0
- package/dist/daemon/routes/alerts.js.map +1 -0
- package/dist/daemon/routes/approvals.d.ts +4 -0
- package/dist/daemon/routes/approvals.js +40 -0
- package/dist/daemon/routes/approvals.js.map +1 -0
- package/dist/daemon/routes/assets.d.ts +3 -0
- package/dist/daemon/routes/assets.js +64 -0
- package/dist/daemon/routes/assets.js.map +1 -0
- package/dist/daemon/routes/board.d.ts +4 -0
- package/dist/daemon/routes/board.js +147 -0
- package/dist/daemon/routes/board.js.map +1 -0
- package/dist/daemon/routes/brain.d.ts +4 -0
- package/dist/daemon/routes/brain.js +270 -0
- package/dist/daemon/routes/brain.js.map +1 -0
- package/dist/daemon/routes/chats.d.ts +4 -0
- package/dist/daemon/routes/chats.js +249 -0
- package/dist/daemon/routes/chats.js.map +1 -0
- package/dist/daemon/routes/clients.d.ts +4 -0
- package/dist/daemon/routes/clients.js +49 -0
- package/dist/daemon/routes/clients.js.map +1 -0
- package/dist/daemon/routes/cloud.d.ts +4 -0
- package/dist/daemon/routes/cloud.js +46 -0
- package/dist/daemon/routes/cloud.js.map +1 -0
- package/dist/daemon/routes/config.d.ts +4 -0
- package/dist/daemon/routes/config.js +35 -0
- package/dist/daemon/routes/config.js.map +1 -0
- package/dist/daemon/routes/context.d.ts +51 -0
- package/dist/daemon/routes/context.js +2 -0
- package/dist/daemon/routes/context.js.map +1 -0
- package/dist/daemon/routes/crews.d.ts +4 -0
- package/dist/daemon/routes/crews.js +149 -0
- package/dist/daemon/routes/crews.js.map +1 -0
- package/dist/daemon/routes/files.d.ts +3 -0
- package/dist/daemon/routes/files.js +253 -0
- package/dist/daemon/routes/files.js.map +1 -0
- package/dist/daemon/routes/git.d.ts +4 -0
- package/dist/daemon/routes/git.js +96 -0
- package/dist/daemon/routes/git.js.map +1 -0
- package/dist/daemon/routes/history.d.ts +4 -0
- package/dist/daemon/routes/history.js +101 -0
- package/dist/daemon/routes/history.js.map +1 -0
- package/dist/daemon/routes/integrations-project.d.ts +3 -0
- package/dist/daemon/routes/integrations-project.js +103 -0
- package/dist/daemon/routes/integrations-project.js.map +1 -0
- package/dist/daemon/routes/integrations.d.ts +3 -0
- package/dist/daemon/routes/integrations.js +111 -0
- package/dist/daemon/routes/integrations.js.map +1 -0
- package/dist/daemon/routes/logs.d.ts +3 -0
- package/dist/daemon/routes/logs.js +40 -0
- package/dist/daemon/routes/logs.js.map +1 -0
- package/dist/daemon/routes/messages.d.ts +4 -0
- package/dist/daemon/routes/messages.js +60 -0
- package/dist/daemon/routes/messages.js.map +1 -0
- package/dist/daemon/routes/observability.d.ts +4 -0
- package/dist/daemon/routes/observability.js +247 -0
- package/dist/daemon/routes/observability.js.map +1 -0
- package/dist/daemon/routes/onboard.d.ts +12 -0
- package/dist/daemon/routes/onboard.js +67 -0
- package/dist/daemon/routes/onboard.js.map +1 -0
- package/dist/daemon/routes/orchestra.d.ts +4 -0
- package/dist/daemon/routes/orchestra.js +110 -0
- package/dist/daemon/routes/orchestra.js.map +1 -0
- package/dist/daemon/routes/pairing.d.ts +4 -0
- package/dist/daemon/routes/pairing.js +155 -0
- package/dist/daemon/routes/pairing.js.map +1 -0
- package/dist/daemon/routes/preview.d.ts +24 -0
- package/dist/daemon/routes/preview.js +122 -0
- package/dist/daemon/routes/preview.js.map +1 -0
- package/dist/daemon/routes/project-actions.d.ts +4 -0
- package/dist/daemon/routes/project-actions.js +53 -0
- package/dist/daemon/routes/project-actions.js.map +1 -0
- package/dist/daemon/routes/project-team.d.ts +4 -0
- package/dist/daemon/routes/project-team.js +201 -0
- package/dist/daemon/routes/project-team.js.map +1 -0
- package/dist/daemon/routes/projects.d.ts +14 -0
- package/dist/daemon/routes/projects.js +138 -0
- package/dist/daemon/routes/projects.js.map +1 -0
- package/dist/daemon/routes/prompts.d.ts +4 -0
- package/dist/daemon/routes/prompts.js +44 -0
- package/dist/daemon/routes/prompts.js.map +1 -0
- package/dist/daemon/routes/proof.d.ts +4 -0
- package/dist/daemon/routes/proof.js +22 -0
- package/dist/daemon/routes/proof.js.map +1 -0
- package/dist/daemon/routes/providers.d.ts +3 -0
- package/dist/daemon/routes/providers.js +60 -0
- package/dist/daemon/routes/providers.js.map +1 -0
- package/dist/daemon/routes/public.d.ts +4 -0
- package/dist/daemon/routes/public.js +64 -0
- package/dist/daemon/routes/public.js.map +1 -0
- package/dist/daemon/routes/queue.d.ts +4 -0
- package/dist/daemon/routes/queue.js +140 -0
- package/dist/daemon/routes/queue.js.map +1 -0
- package/dist/daemon/routes/routing.d.ts +4 -0
- package/dist/daemon/routes/routing.js +61 -0
- package/dist/daemon/routes/routing.js.map +1 -0
- package/dist/daemon/routes/runtime-handler.d.ts +2 -0
- package/dist/daemon/routes/runtime-handler.js +63 -0
- package/dist/daemon/routes/runtime-handler.js.map +1 -0
- package/dist/daemon/routes/search.d.ts +4 -0
- package/dist/daemon/routes/search.js +17 -0
- package/dist/daemon/routes/search.js.map +1 -0
- package/dist/daemon/routes/servers.d.ts +4 -0
- package/dist/daemon/routes/servers.js +133 -0
- package/dist/daemon/routes/servers.js.map +1 -0
- package/dist/daemon/routes/setup.d.ts +3 -0
- package/dist/daemon/routes/setup.js +50 -0
- package/dist/daemon/routes/setup.js.map +1 -0
- package/dist/daemon/routes/shift.d.ts +4 -0
- package/dist/daemon/routes/shift.js +30 -0
- package/dist/daemon/routes/shift.js.map +1 -0
- package/dist/daemon/routes/specs.d.ts +4 -0
- package/dist/daemon/routes/specs.js +57 -0
- package/dist/daemon/routes/specs.js.map +1 -0
- package/dist/daemon/routes/task-delivery.d.ts +4 -0
- package/dist/daemon/routes/task-delivery.js +38 -0
- package/dist/daemon/routes/task-delivery.js.map +1 -0
- package/dist/daemon/routes/team.d.ts +4 -0
- package/dist/daemon/routes/team.js +193 -0
- package/dist/daemon/routes/team.js.map +1 -0
- package/dist/daemon/routes/terminals.d.ts +4 -0
- package/dist/daemon/routes/terminals.js +87 -0
- package/dist/daemon/routes/terminals.js.map +1 -0
- package/dist/daemon/routes/tools.d.ts +4 -0
- package/dist/daemon/routes/tools.js +180 -0
- package/dist/daemon/routes/tools.js.map +1 -0
- package/dist/daemon/routes/updates.d.ts +4 -0
- package/dist/daemon/routes/updates.js +89 -0
- package/dist/daemon/routes/updates.js.map +1 -0
- package/dist/daemon/routes/voice.d.ts +4 -0
- package/dist/daemon/routes/voice.js +53 -0
- package/dist/daemon/routes/voice.js.map +1 -0
- package/dist/daemon/runner.d.ts +186 -0
- package/dist/daemon/runner.js +612 -0
- package/dist/daemon/runner.js.map +1 -0
- package/dist/daemon/runtime/accounting.d.ts +130 -0
- package/dist/daemon/runtime/accounting.js +303 -0
- package/dist/daemon/runtime/accounting.js.map +1 -0
- package/dist/daemon/runtime/agents.d.ts +16 -0
- package/dist/daemon/runtime/agents.js +71 -0
- package/dist/daemon/runtime/agents.js.map +1 -0
- package/dist/daemon/runtime/briefings.d.ts +134 -0
- package/dist/daemon/runtime/briefings.js +329 -0
- package/dist/daemon/runtime/briefings.js.map +1 -0
- package/dist/daemon/runtime/native-usage.d.ts +21 -0
- package/dist/daemon/runtime/native-usage.js +71 -0
- package/dist/daemon/runtime/native-usage.js.map +1 -0
- package/dist/daemon/runtime/queue.d.ts +94 -0
- package/dist/daemon/runtime/queue.js +222 -0
- package/dist/daemon/runtime/queue.js.map +1 -0
- package/dist/daemon/runtime/relay.d.ts +93 -0
- package/dist/daemon/runtime/relay.js +240 -0
- package/dist/daemon/runtime/relay.js.map +1 -0
- package/dist/daemon/runtime/turns.d.ts +202 -0
- package/dist/daemon/runtime/turns.js +663 -0
- package/dist/daemon/runtime/turns.js.map +1 -0
- package/dist/daemon/runtime-support.d.ts +76 -0
- package/dist/daemon/runtime-support.js +134 -0
- package/dist/daemon/runtime-support.js.map +1 -0
- package/dist/daemon/runtime.d.ts +739 -70
- package/dist/daemon/runtime.js +2751 -470
- package/dist/daemon/runtime.js.map +1 -1
- package/dist/daemon/server.d.ts +82 -55
- package/dist/daemon/server.js +674 -1435
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/shift.d.ts +70 -0
- package/dist/daemon/shift.js +170 -0
- package/dist/daemon/shift.js.map +1 -0
- package/dist/daemon/specs.d.ts +92 -0
- package/dist/daemon/specs.js +285 -0
- package/dist/daemon/specs.js.map +1 -0
- package/dist/daemon/system.d.ts +208 -0
- package/dist/daemon/system.js +557 -0
- package/dist/daemon/system.js.map +1 -0
- package/dist/daemon/tasks.js +1 -1
- package/dist/daemon/team-brain.d.ts +136 -0
- package/dist/daemon/team-brain.js +659 -0
- package/dist/daemon/team-brain.js.map +1 -0
- package/dist/daemon/team-coordinator.d.ts +78 -0
- package/dist/daemon/team-coordinator.js +408 -0
- package/dist/daemon/team-coordinator.js.map +1 -0
- package/dist/daemon/team.d.ts +339 -0
- package/dist/daemon/team.js +1433 -0
- package/dist/daemon/team.js.map +1 -0
- package/dist/daemon/terminals.d.ts +22 -3
- package/dist/daemon/terminals.js +236 -16
- package/dist/daemon/terminals.js.map +1 -1
- package/dist/hub/client.d.ts +120 -0
- package/dist/hub/client.js +228 -0
- package/dist/hub/client.js.map +1 -0
- package/dist/hub/server.d.ts +32 -0
- package/dist/hub/server.js +180 -0
- package/dist/hub/server.js.map +1 -0
- package/dist/hub/supabase-client.d.ts +220 -0
- package/dist/hub/supabase-client.js +843 -0
- package/dist/hub/supabase-client.js.map +1 -0
- package/dist/observability/ask.d.ts +91 -0
- package/dist/observability/ask.js +179 -0
- package/dist/observability/ask.js.map +1 -0
- package/dist/observability/clickhouse.d.ts +33 -0
- package/dist/observability/clickhouse.js +48 -0
- package/dist/observability/clickhouse.js.map +1 -0
- package/dist/observability/decisions.d.ts +176 -0
- package/dist/observability/decisions.js +377 -0
- package/dist/observability/decisions.js.map +1 -0
- package/dist/observability/index.d.ts +89 -0
- package/dist/observability/index.js +348 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/insights.d.ts +151 -0
- package/dist/observability/insights.js +319 -0
- package/dist/observability/insights.js.map +1 -0
- package/dist/observability/logs-query.d.ts +82 -0
- package/dist/observability/logs-query.js +102 -0
- package/dist/observability/logs-query.js.map +1 -0
- package/dist/observability/logs.d.ts +72 -0
- package/dist/observability/logs.js +295 -0
- package/dist/observability/logs.js.map +1 -0
- package/dist/observability/metrics.d.ts +86 -0
- package/dist/observability/metrics.js +303 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/observability/otlp.d.ts +127 -0
- package/dist/observability/otlp.js +242 -0
- package/dist/observability/otlp.js.map +1 -0
- package/dist/observability/snapshots.d.ts +43 -0
- package/dist/observability/snapshots.js +129 -0
- package/dist/observability/snapshots.js.map +1 -0
- package/dist/observability/triage.d.ts +49 -0
- package/dist/observability/triage.js +284 -0
- package/dist/observability/triage.js.map +1 -0
- package/dist/providers/adapter.d.ts +50 -0
- package/dist/providers/adapter.js +38 -0
- package/dist/providers/adapter.js.map +1 -0
- package/dist/providers/agent.d.ts +110 -0
- package/dist/providers/agent.js +487 -0
- package/dist/providers/agent.js.map +1 -0
- package/dist/providers/approvals.d.ts +24 -0
- package/dist/providers/approvals.js +78 -0
- package/dist/providers/approvals.js.map +1 -0
- package/dist/providers/attachments.d.ts +20 -0
- package/dist/providers/attachments.js +51 -0
- package/dist/providers/attachments.js.map +1 -0
- package/dist/providers/claude/adapter.d.ts +99 -0
- package/dist/providers/claude/adapter.js +790 -0
- package/dist/providers/claude/adapter.js.map +1 -0
- package/dist/providers/codex/adapter.d.ts +113 -0
- package/dist/providers/codex/adapter.js +881 -0
- package/dist/providers/codex/adapter.js.map +1 -0
- package/dist/providers/codex/instructions.d.ts +11 -0
- package/dist/providers/codex/instructions.js +154 -0
- package/dist/providers/codex/instructions.js.map +1 -0
- package/dist/providers/codex/rpc.d.ts +36 -0
- package/dist/providers/codex/rpc.js +104 -0
- package/dist/providers/codex/rpc.js.map +1 -0
- package/dist/providers/contracts.d.ts +339 -0
- package/dist/providers/contracts.js +19 -0
- package/dist/providers/contracts.js.map +1 -0
- package/dist/providers/directory.d.ts +70 -0
- package/dist/providers/directory.js +95 -0
- package/dist/providers/directory.js.map +1 -0
- package/dist/providers/errors.d.ts +47 -0
- package/dist/providers/errors.js +24 -0
- package/dist/providers/errors.js.map +1 -0
- package/dist/providers/index.d.ts +18 -0
- package/dist/providers/index.js +19 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/ingestion.d.ts +80 -0
- package/dist/providers/ingestion.js +330 -0
- package/dist/providers/ingestion.js.map +1 -0
- package/dist/providers/live.d.ts +22 -0
- package/dist/providers/live.js +56 -0
- package/dist/providers/live.js.map +1 -0
- package/dist/providers/process.d.ts +29 -0
- package/dist/providers/process.js +71 -0
- package/dist/providers/process.js.map +1 -0
- package/dist/providers/reaper.d.ts +33 -0
- package/dist/providers/reaper.js +65 -0
- package/dist/providers/reaper.js.map +1 -0
- package/dist/providers/service.d.ts +87 -0
- package/dist/providers/service.js +256 -0
- package/dist/providers/service.js.map +1 -0
- package/dist/sdk.d.ts +1 -1
- package/dist/sdk.js +1 -1
- package/dist/sdk.js.map +1 -1
- package/dist/types.d.ts +385 -6
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +7 -0
- package/dist/version.js +16 -0
- package/dist/version.js.map +1 -0
- package/dist/web/app.css +4643 -0
- package/dist/web/app.js +22640 -0
- package/dist/web/app.js.map +7 -0
- package/dist/web/shell.html +39 -0
- package/docs/README.md +21 -0
- package/docs/SETUP.md +10 -0
- package/docs/adapters.md +21 -8
- package/docs/brain-continuity.md +186 -0
- package/docs/cloud.md +108 -0
- package/docs/crews.md +62 -0
- package/docs/design-system.md +31 -2
- package/docs/integration-notes.md +128 -20
- package/docs/loompad.md +105 -0
- package/docs/observability.md +220 -0
- package/docs/play-store.md +123 -0
- package/docs/proposals/agent-teams.md +249 -0
- package/docs/proposals/brain-continuity-implementation.md +468 -0
- package/docs/proposals/brain-session-continuity.md +623 -0
- package/docs/proposals/t3code-port.md +81 -0
- package/docs/proposals/t3code-reference.md +95 -0
- package/docs/refactoring/AGENT-TEAMS-PROGRESS.md +88 -0
- package/docs/refactoring/BOUNDARIES.md +120 -0
- package/docs/refactoring/BRAIN-AUDIT.md +176 -0
- package/docs/refactoring/BRAIN-NOTES.md +390 -0
- package/docs/refactoring/BRAIN-TODO.md +214 -0
- package/docs/refactoring/NOTES.md +129 -0
- package/docs/refactoring/T3-PORT-NOTES.md +165 -0
- package/docs/refactoring/T3-PORT-TODO.md +123 -0
- package/docs/refactoring/TODO.md +46 -0
- package/docs/refactoring/brain-benchmark-2026-09-28.json +48 -0
- package/docs/refactoring/brain-benchmark-audit-2026-09-28.json +48 -0
- package/docs/release-notes/v0.2.0.md +35 -0
- package/docs/release-notes/v0.2.1.md +59 -0
- package/docs/release-notes/v0.2.3.md +77 -0
- package/docs/release-notes/v0.2.4.md +128 -0
- package/docs/release-notes/v0.2.5.md +153 -0
- package/docs/release-notes/v0.2.6.md +62 -0
- package/docs/release-notes/v1.0.0.md +120 -0
- package/docs/release-notes/v1.0.1.md +77 -0
- package/docs/release-notes/v1.1.0.md +71 -0
- package/docs/release-notes/v1.1.1.md +38 -0
- package/docs/teams-architecture.md +697 -0
- package/docs/teams.md +171 -0
- package/docs/windows.md +85 -0
- package/package.json +22 -9
- package/dist/adapters/claude-code.d.ts +0 -26
- package/dist/adapters/claude-code.js +0 -209
- package/dist/adapters/claude-code.js.map +0 -1
- package/dist/adapters/codex.d.ts +0 -51
- package/dist/adapters/codex.js +0 -291
- package/dist/adapters/codex.js.map +0 -1
- package/docs/img/board-linear.png +0 -0
- package/docs/img/board-projects.png +0 -0
- package/docs/img/brain.png +0 -0
- package/docs/img/settings.png +0 -0
- package/docs/img/workspace.png +0 -0
|
@@ -9,10 +9,35 @@
|
|
|
9
9
|
* live events : GET /event (SSE)
|
|
10
10
|
*
|
|
11
11
|
* Surface verified against opencode 1.17.20 — see docs/integration-notes.md.
|
|
12
|
+
*
|
|
13
|
+
* Native continuity (see core/continuity): a continuity turn runs on the
|
|
14
|
+
* session its binding names, or a new one it reports as `turn_started`; a
|
|
15
|
+
* bound session the server no longer has is NativeSessionMissing, before
|
|
16
|
+
* anything is sent. The prompt's admission is the acceptance evidence
|
|
17
|
+
* (`native_turn_accepted`), and the turn is over — and quiescent — when
|
|
18
|
+
* `/api/session/active` no longer lists the session. Verified against
|
|
19
|
+
* opencode 1.18.31 (`/api/session/{id}/model` switches the model in place,
|
|
20
|
+
* `session.next.compaction.*` reports compaction).
|
|
12
21
|
*/
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
22
|
+
import { attachedImages } from "../providers/attachments.js";
|
|
23
|
+
import { execFile, spawnContained, stopWindowsTree, stopWindowsPid } from "../core/process.js";
|
|
24
|
+
import { windowsJobReady } from "../core/windows-jobs.js";
|
|
25
|
+
import { createHash } from "node:crypto";
|
|
26
|
+
import fs from "node:fs";
|
|
27
|
+
import path from "node:path";
|
|
28
|
+
import { AdapterBase, agentEnv, cliAvailable, cliOutput, fetchJson, firstLine, frameBriefing, freePort, interruptProcess, waitFor } from "./base.js";
|
|
29
|
+
import { requestApproval } from "../core/approvals.js";
|
|
30
|
+
import { logbook } from "../core/logbook.js";
|
|
31
|
+
import { permissionFor } from "../core/permissions.js";
|
|
32
|
+
import { NativeDispatchRejected, NativeQuiescenceUnknown, NativeSessionMissing } from "../core/continuity/contracts.js";
|
|
33
|
+
/** OPENCODE_IDLE_MS overrides it (tests); otherwise the agent's idleMinutes, else 30 minutes. */
|
|
34
|
+
function idleMsFor(options) {
|
|
35
|
+
const env = Number(process.env.LOOM_OPENCODE_IDLE_MS);
|
|
36
|
+
if (Number.isFinite(env) && env >= 0 && process.env.LOOM_OPENCODE_IDLE_MS !== undefined)
|
|
37
|
+
return env;
|
|
38
|
+
const minutes = typeof options.idleMinutes === "number" && options.idleMinutes >= 0 ? options.idleMinutes : 30;
|
|
39
|
+
return minutes * 60_000;
|
|
40
|
+
}
|
|
16
41
|
/** "providerID/modelID" → ModelRef body for session create ({providerID, id}). */
|
|
17
42
|
export function parseModelRef(model) {
|
|
18
43
|
const idx = model.indexOf("/");
|
|
@@ -20,59 +45,226 @@ export function parseModelRef(model) {
|
|
|
20
45
|
return null;
|
|
21
46
|
return { providerID: model.slice(0, idx), id: model.slice(idx + 1) };
|
|
22
47
|
}
|
|
48
|
+
/** Errors that come from something already in the session's history, not from this turn. */
|
|
49
|
+
const POISONED_HISTORY = /media must contain valid base64|invalid (?:image|base64)|could not process image|image (?:is )?(?:too large|not supported)/i;
|
|
50
|
+
function turnError(error) {
|
|
51
|
+
return String(error.message ?? error.data?.message ?? "opencode turn failed").slice(0, 500);
|
|
52
|
+
}
|
|
53
|
+
/** The prompt body, with attached pictures as file attachments so OpenCode sees them, not just their path. */
|
|
54
|
+
function promptWithFiles(text, dir) {
|
|
55
|
+
// inline as a data URL: a file:// URL to a folder with a space in it ("Extreme SSD") reached the model as empty media
|
|
56
|
+
const files = attachedImages(text, dir).map((im) => ({ uri: `data:${im.mime};base64,${fs.readFileSync(im.abs).toString("base64")}`, name: path.basename(im.rel) }));
|
|
57
|
+
return files.length ? { text, files } : { text };
|
|
58
|
+
}
|
|
59
|
+
/** A turn's tokens for run_complete: totals, plus the cached and reasoning shares when there are any. */
|
|
60
|
+
function usagePayload(u) {
|
|
61
|
+
return {
|
|
62
|
+
inputTokens: u.input,
|
|
63
|
+
outputTokens: u.output,
|
|
64
|
+
...(u.cached ? { cachedInputTokens: u.cached } : {}),
|
|
65
|
+
...(u.reasoning ? { reasoningTokens: u.reasoning } : {}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The model to use when the project pins none: opencode's free house model if
|
|
70
|
+
* the server offers it, else any free opencode-hosted model, else nothing
|
|
71
|
+
* (opencode's own default — the old behaviour).
|
|
72
|
+
*/
|
|
73
|
+
export function pickDefaultModel(ids) {
|
|
74
|
+
if (ids.includes("opencode/big-pickle"))
|
|
75
|
+
return "opencode/big-pickle";
|
|
76
|
+
return ids.find((id) => id.startsWith("opencode/") && id.endsWith("-free"));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Permissions (core/permissions.ts) for the spawned server, via opencode's
|
|
80
|
+
* inline-config variable — the user's own opencode.json is never touched.
|
|
81
|
+
*/
|
|
82
|
+
export function opencodePermissionEnv(options) {
|
|
83
|
+
// Only bypass changes anything. "ask" is refused for opencode upstream (see
|
|
84
|
+
// core/permissions.ts): its headless session API ignored a deny-everything
|
|
85
|
+
// config and the read-only `plan` agent alike — a file write went through
|
|
86
|
+
// both (verified on opencode 1.18.31) — so a read-only switch here would lie.
|
|
87
|
+
if (permissionFor("opencode", options) !== "bypass")
|
|
88
|
+
return {};
|
|
89
|
+
return {
|
|
90
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
91
|
+
permission: { edit: "allow", bash: "allow", webfetch: "allow", external_directory: "allow" },
|
|
92
|
+
}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
23
95
|
export class OpenCodeAdapter extends AdapterBase {
|
|
24
96
|
child = null;
|
|
25
97
|
baseUrl = null;
|
|
26
98
|
sseAbort = null;
|
|
27
99
|
options;
|
|
28
100
|
started = false;
|
|
101
|
+
/** npm builds expose /session + /global/health; newer builds use /api. */
|
|
102
|
+
modernApi = false;
|
|
103
|
+
/**
|
|
104
|
+
* Before a turn: whatever a previous turn left in flight (an interrupted
|
|
105
|
+
* reply's parts, a prompt's echo) is never coming back, and the dedupe sets
|
|
106
|
+
* only need recent ids — so they're trimmed, not kept for the agent's life.
|
|
107
|
+
*/
|
|
108
|
+
trimTurnState() {
|
|
109
|
+
this.textParts.clear();
|
|
110
|
+
this.reasoningParts.clear();
|
|
111
|
+
const keep = (set, max = 500) => {
|
|
112
|
+
if (set.size <= max)
|
|
113
|
+
return;
|
|
114
|
+
let drop = set.size - max;
|
|
115
|
+
for (const k of set.keys()) {
|
|
116
|
+
if (drop-- <= 0)
|
|
117
|
+
break;
|
|
118
|
+
set.delete(k);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
keep(this.roles);
|
|
122
|
+
keep(this.emittedText);
|
|
123
|
+
keep(this.toolsDone);
|
|
124
|
+
}
|
|
125
|
+
/** Stops an idle server Loom started (see idleMinutes). */
|
|
126
|
+
idleTimer = null;
|
|
127
|
+
cancelIdle() {
|
|
128
|
+
if (this.idleTimer)
|
|
129
|
+
clearTimeout(this.idleTimer);
|
|
130
|
+
this.idleTimer = null;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* After a turn: stop the server we spawned once nothing has used it for a
|
|
134
|
+
* while. Never one we were pointed at (baseUrl), never mid-turn, and never
|
|
135
|
+
* while a question it asked is still waiting for an answer.
|
|
136
|
+
*/
|
|
137
|
+
armIdle() {
|
|
138
|
+
this.cancelIdle();
|
|
139
|
+
const ms = idleMsFor(this.options);
|
|
140
|
+
if (!ms || this.options.baseUrl || !this.child)
|
|
141
|
+
return;
|
|
142
|
+
this.idleTimer = setTimeout(() => {
|
|
143
|
+
this.idleTimer = null;
|
|
144
|
+
if (this._busy || this.questions.size || !this.child || !this.started)
|
|
145
|
+
return void this.armIdle();
|
|
146
|
+
logbook.info("opencode", `stopping idle opencode serve for "${this.id}" after ${Math.round(ms / 60_000)} min; the next turn restarts it`);
|
|
147
|
+
void this.sleep().catch(() => { });
|
|
148
|
+
}, ms);
|
|
149
|
+
this.idleTimer.unref?.();
|
|
150
|
+
}
|
|
151
|
+
/** Stop the idle server quietly: not a failure, and not the agent stopping. */
|
|
152
|
+
async sleep() {
|
|
153
|
+
this.started = false;
|
|
154
|
+
this.sseAbort?.abort();
|
|
155
|
+
this.sseAbort = null;
|
|
156
|
+
const child = this.child;
|
|
157
|
+
this.child = null;
|
|
158
|
+
if (!child)
|
|
159
|
+
return;
|
|
160
|
+
if (process.platform === "win32")
|
|
161
|
+
await stopWindowsTree(child);
|
|
162
|
+
else
|
|
163
|
+
await interruptProcess(child, true);
|
|
164
|
+
this.emit({ kind: "status", payload: { state: "idle_stopped" } });
|
|
165
|
+
}
|
|
166
|
+
api(route) {
|
|
167
|
+
return `${this.baseUrl}${this.modernApi ? "" : "/api"}${route}`;
|
|
168
|
+
}
|
|
169
|
+
promptBody(text) {
|
|
170
|
+
if (!this.modernApi)
|
|
171
|
+
return { prompt: promptWithFiles(text, this.projectDir) };
|
|
172
|
+
const ref = this.options.model ? parseModelRef(this.options.model) : null;
|
|
173
|
+
return {
|
|
174
|
+
parts: [{ type: "text", text }, ...attachedImages(text, this.projectDir).map(im => ({
|
|
175
|
+
type: "file", mime: im.mime, filename: path.basename(im.abs), url: `data:${im.mime};base64,${fs.readFileSync(im.abs).toString("base64")}`,
|
|
176
|
+
}))],
|
|
177
|
+
...(ref ? { model: { providerID: ref.providerID, modelID: ref.id } } : {}),
|
|
178
|
+
...(this.options.agent ? { agent: this.options.agent } : {}),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
async messageDetail(sid, mid) {
|
|
182
|
+
const detail = await fetchJson(this.api(`/session/${sid}/message/${mid}`)).catch(() => null);
|
|
183
|
+
if (this.modernApi && detail?.info)
|
|
184
|
+
return { ...detail.info, content: detail.parts };
|
|
185
|
+
return detail;
|
|
186
|
+
}
|
|
187
|
+
// Token usage from the assistant message, stashed to ride run_complete.
|
|
188
|
+
lastUsage = null;
|
|
189
|
+
// The provider/model the assistant message actually ran, for the gen_ai span.
|
|
190
|
+
lastModel = null;
|
|
29
191
|
/** text parts per in-flight assistant message */
|
|
30
192
|
textParts = new Map();
|
|
31
193
|
roles = new Map();
|
|
32
194
|
/** assistant messages whose text already went to the log (SSE path) */
|
|
33
195
|
emittedText = new Set();
|
|
196
|
+
/** The session a continuity turn is running on; the agent's own otherwise. */
|
|
197
|
+
activeSid;
|
|
198
|
+
/** interrupt() was asked for during this turn. */
|
|
199
|
+
interrupted = false;
|
|
200
|
+
/** Tools opencode started this turn, by call id (1.18 reports name and result separately). */
|
|
201
|
+
toolCalls = new Map();
|
|
202
|
+
/** A compaction was already reported as done (both the step event and the session event can say so). */
|
|
203
|
+
compactionReported = false;
|
|
34
204
|
constructor(id, projectDir, options = {}) {
|
|
35
205
|
super(id, "opencode", projectDir);
|
|
36
206
|
this.options = options;
|
|
37
207
|
}
|
|
38
208
|
get sessionId() {
|
|
39
|
-
return
|
|
209
|
+
return this.nativeState.read().sessionId;
|
|
40
210
|
}
|
|
41
211
|
set sessionId(value) {
|
|
42
|
-
|
|
43
|
-
state.agents[this.id] = { ...state.agents[this.id], sessionId: value };
|
|
44
|
-
writeProjectState(this.projectDir, state);
|
|
212
|
+
this.nativeState.patch({ sessionId: value });
|
|
45
213
|
}
|
|
46
214
|
async available() {
|
|
47
215
|
if (this.options.baseUrl)
|
|
48
216
|
return true;
|
|
49
217
|
return cliAvailable("opencode");
|
|
50
218
|
}
|
|
219
|
+
async selfCheck() {
|
|
220
|
+
if (this.options.baseUrl)
|
|
221
|
+
return [{ name: "server", ok: true, detail: `uses the opencode server at ${this.options.baseUrl}` }];
|
|
222
|
+
const v = await cliOutput("opencode", ["--version"]);
|
|
223
|
+
if (v?.code !== 0)
|
|
224
|
+
return [{ name: "installed", ok: false, detail: "opencode not found on this machine — install it" }];
|
|
225
|
+
const checks = [{ name: "installed", ok: true, detail: `opencode ${firstLine(v.out)}` }];
|
|
226
|
+
const a = await cliOutput("opencode", ["auth", "list"]);
|
|
227
|
+
const providers = (a?.out ?? "")
|
|
228
|
+
.split("\n")
|
|
229
|
+
.map((l) => /[●•]\s+(.+?)(?:\s{2,}|\s+(?:api|oauth|wellknown)\s*$|$)/.exec(l)?.[1]?.trim())
|
|
230
|
+
.filter((x) => !!x)
|
|
231
|
+
// the environment section names the variable too ("OpenRouter OPENROUTER_API_KEY")
|
|
232
|
+
.map((x) => x.replace(/\s+[A-Z][A-Z0-9_]{2,}$/, ""))
|
|
233
|
+
.filter((x, i, all) => all.indexOf(x) === i);
|
|
234
|
+
checks.push({
|
|
235
|
+
name: "providers",
|
|
236
|
+
// opencode's own free models need no sign-in, so none is a note, not a failure
|
|
237
|
+
ok: true,
|
|
238
|
+
detail: providers.length ? `signed in to ${providers.join(", ")}` : "no providers signed in — its free models still work",
|
|
239
|
+
});
|
|
240
|
+
return checks;
|
|
241
|
+
}
|
|
51
242
|
/** Kill a serve child left behind by a previous daemon (verified by cmdline). */
|
|
52
243
|
async reapOrphanServe() {
|
|
53
|
-
const
|
|
54
|
-
const pid = Number(state.agents[this.id]?.servePid ?? 0);
|
|
244
|
+
const pid = Number(this.nativeState.read().servePid ?? 0);
|
|
55
245
|
if (!pid)
|
|
56
246
|
return;
|
|
57
247
|
const cmd = await new Promise((resolve) => {
|
|
58
|
-
|
|
248
|
+
const windows = process.platform === "win32";
|
|
249
|
+
execFile(windows ? "powershell.exe" : "ps", windows ? ["-NoProfile", "-NonInteractive", "-Command", `(Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}').CommandLine`] : ["-p", String(pid), "-o", "command="], { timeout: 8000 }, (err, stdout) => resolve(err ? "" : stdout.trim()));
|
|
59
250
|
});
|
|
60
|
-
if (/opencode
|
|
251
|
+
if (/opencode(?:\.exe)?["\s]+serve\b/i.test(cmd)) {
|
|
61
252
|
try {
|
|
62
|
-
process.
|
|
253
|
+
if (process.platform === "win32")
|
|
254
|
+
await stopWindowsPid(pid);
|
|
255
|
+
else
|
|
256
|
+
process.kill(pid, "SIGTERM");
|
|
63
257
|
this.emit({ kind: "status", payload: { state: "reaped_orphan_serve", pid } });
|
|
64
258
|
}
|
|
65
259
|
catch {
|
|
66
260
|
// already gone
|
|
67
261
|
}
|
|
68
262
|
}
|
|
69
|
-
|
|
70
|
-
|
|
263
|
+
if (this.nativeState.read().servePid === pid)
|
|
264
|
+
this.nativeState.patch({ servePid: undefined });
|
|
71
265
|
}
|
|
72
266
|
recordServePid(pid) {
|
|
73
|
-
|
|
74
|
-
state.agents[this.id] = { ...state.agents[this.id], servePid: pid };
|
|
75
|
-
writeProjectState(this.projectDir, state);
|
|
267
|
+
this.nativeState.patch({ servePid: pid });
|
|
76
268
|
}
|
|
77
269
|
async start() {
|
|
78
270
|
if (this.started)
|
|
@@ -84,22 +276,41 @@ export class OpenCodeAdapter extends AdapterBase {
|
|
|
84
276
|
await this.reapOrphanServe();
|
|
85
277
|
const port = await freePort();
|
|
86
278
|
this.baseUrl = `http://127.0.0.1:${port}`;
|
|
87
|
-
const child =
|
|
279
|
+
const child = spawnContained("opencode", ["serve", "--port", String(port), "--hostname", "127.0.0.1", ...(this.options.extraArgs ?? [])], { cwd: this.projectDir, stdio: "ignore", env: { ...agentEnv(), ...opencodePermissionEnv(this.options) } });
|
|
88
280
|
this.child = child;
|
|
281
|
+
child.on("error", (error) => this.emit({ kind: "error", payload: { message: `opencode serve could not start: ${error.message}` } }));
|
|
89
282
|
this.recordServePid(child.pid);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
283
|
+
if (process.platform === "win32")
|
|
284
|
+
await windowsJobReady(child);
|
|
285
|
+
child.on("close", (code, signal) => {
|
|
286
|
+
if (!this.started)
|
|
287
|
+
return;
|
|
288
|
+
this.started = false;
|
|
289
|
+
// Killed by a signal with no turn running is the daemon shutting down
|
|
290
|
+
// (or restarting) around it — not a failure, and not worth a red card
|
|
291
|
+
// in the thread. The next send starts a fresh server either way.
|
|
292
|
+
if (code === null && !this._busy) {
|
|
293
|
+
this.emit({ kind: "status", payload: { state: "stopped", signal: signal ?? null } });
|
|
294
|
+
return;
|
|
94
295
|
}
|
|
296
|
+
this.emit({ kind: "error", payload: { message: `opencode serve exited (${code ?? signal})` } });
|
|
95
297
|
});
|
|
96
298
|
}
|
|
97
299
|
await waitFor(async () => {
|
|
98
|
-
|
|
300
|
+
try {
|
|
301
|
+
await fetchJson(`${this.baseUrl}/api/health`);
|
|
302
|
+
this.modernApi = false;
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
await fetchJson(`${this.baseUrl}/global/health`);
|
|
306
|
+
this.modernApi = true;
|
|
307
|
+
}
|
|
99
308
|
return true;
|
|
100
|
-
});
|
|
309
|
+
}, { timeoutMs: process.platform === "win32" ? 60_000 : 30_000 });
|
|
101
310
|
await this.assertModelAvailable();
|
|
102
|
-
|
|
311
|
+
// No session yet: an ordinary turn makes (or reuses) the agent's own when it
|
|
312
|
+
// sends, and a continuity turn runs on its binding's. Creating one here left
|
|
313
|
+
// an empty session behind every time a continuity project started.
|
|
103
314
|
this.startSse();
|
|
104
315
|
this.started = true;
|
|
105
316
|
this.emit({
|
|
@@ -108,12 +319,10 @@ export class OpenCodeAdapter extends AdapterBase {
|
|
|
108
319
|
});
|
|
109
320
|
}
|
|
110
321
|
get sessionModel() {
|
|
111
|
-
return
|
|
322
|
+
return this.nativeState.read().sessionModel;
|
|
112
323
|
}
|
|
113
324
|
set sessionModel(value) {
|
|
114
|
-
|
|
115
|
-
state.agents[this.id] = { ...state.agents[this.id], sessionModel: value };
|
|
116
|
-
writeProjectState(this.projectDir, state);
|
|
325
|
+
this.nativeState.patch({ sessionModel: value });
|
|
117
326
|
}
|
|
118
327
|
/**
|
|
119
328
|
* A session whose model can't be resolved is a silent trap: opencode
|
|
@@ -122,13 +331,21 @@ export class OpenCodeAdapter extends AdapterBase {
|
|
|
122
331
|
* server can actually run — and fail loudly with suggestions instead.
|
|
123
332
|
*/
|
|
124
333
|
async assertModelAvailable() {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
334
|
+
const res = await fetchJson(this.modernApi ? `${this.baseUrl}/provider` : `${this.baseUrl}/api/model`).catch(() => null);
|
|
335
|
+
const connected = Array.isArray(res?.connected) ? res.connected : [];
|
|
336
|
+
const models = this.modernApi && Array.isArray(res?.all)
|
|
337
|
+
? res.all.filter(p => connected.includes(String(p.id))).flatMap(p => Object.keys((p.models ?? {})).map(id => ({ providerID: p.id, id })))
|
|
338
|
+
: Array.isArray(res?.data) ? res.data : [];
|
|
129
339
|
if (!models.length)
|
|
130
340
|
return; // endpoint unavailable — don't block
|
|
131
341
|
const ids = models.map((m) => `${String(m.providerID)}/${String(m.id)}`);
|
|
342
|
+
if (!this.options.model) {
|
|
343
|
+
// No pin: opencode's own default is often a model its Console won't
|
|
344
|
+
// serve headless ("Model is unavailable" on the first turn, verified on
|
|
345
|
+
// opencode 1.18.31). Pick one the server says it can run instead.
|
|
346
|
+
this.options.model = pickDefaultModel(ids);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
132
349
|
if (ids.includes(this.options.model))
|
|
133
350
|
return;
|
|
134
351
|
const base = this.options.model.split("/").pop().split("-")[0].toLowerCase();
|
|
@@ -145,7 +362,7 @@ export class OpenCodeAdapter extends AdapterBase {
|
|
|
145
362
|
const modelChanged = (this.options.model ?? undefined) !== this.sessionModel;
|
|
146
363
|
if (existing && !modelChanged) {
|
|
147
364
|
try {
|
|
148
|
-
await fetchJson(
|
|
365
|
+
await fetchJson(this.api(`/session/${existing}`));
|
|
149
366
|
return existing;
|
|
150
367
|
}
|
|
151
368
|
catch {
|
|
@@ -160,7 +377,7 @@ export class OpenCodeAdapter extends AdapterBase {
|
|
|
160
377
|
}
|
|
161
378
|
if (this.options.agent)
|
|
162
379
|
body.agent = this.options.agent;
|
|
163
|
-
const res = await fetchJson(
|
|
380
|
+
const res = await fetchJson(this.api("/session"), {
|
|
164
381
|
method: "POST",
|
|
165
382
|
headers: { "content-type": "application/json" },
|
|
166
383
|
body: JSON.stringify(body),
|
|
@@ -215,7 +432,22 @@ export class OpenCodeAdapter extends AdapterBase {
|
|
|
215
432
|
const type = String(evt.type ?? "");
|
|
216
433
|
// Payload wrapping varies across opencode builds: {properties} or {data}.
|
|
217
434
|
const props = (evt.properties ?? evt.data ?? evt);
|
|
218
|
-
const mySession = this.sessionId;
|
|
435
|
+
const mySession = this.activeSid ?? this.sessionId;
|
|
436
|
+
if (type.startsWith("session.next.tool.") || type.startsWith("session.next.compaction.") || type === "session.compacted") {
|
|
437
|
+
if (props.sessionID && props.sessionID !== mySession)
|
|
438
|
+
return;
|
|
439
|
+
this.handleSessionNext(type, props);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
// opencode 1.18+ streams the reply as session.next.{text,reasoning}.delta,
|
|
443
|
+
// one fragment each; the finished message still arrives the usual way.
|
|
444
|
+
if (/^session\.next\.(text|reasoning)\.delta$/.test(type)) {
|
|
445
|
+
if (props.sessionID && props.sessionID !== mySession)
|
|
446
|
+
return;
|
|
447
|
+
if (typeof props.delta === "string")
|
|
448
|
+
this.streamText(props.delta, type.includes("reasoning"));
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
219
451
|
if (type === "message.part.updated") {
|
|
220
452
|
const part = (props.part ?? {});
|
|
221
453
|
if (part.sessionID && part.sessionID !== mySession)
|
|
@@ -225,18 +457,54 @@ export class OpenCodeAdapter extends AdapterBase {
|
|
|
225
457
|
if (partType === "text" && typeof part.text === "string") {
|
|
226
458
|
if (!this.textParts.has(messageID))
|
|
227
459
|
this.textParts.set(messageID, new Map());
|
|
228
|
-
this.textParts.get(messageID)
|
|
460
|
+
const parts = this.textParts.get(messageID);
|
|
461
|
+
const partId = String(part.id ?? "p");
|
|
462
|
+
const prev = parts.get(partId) ?? "";
|
|
463
|
+
parts.set(partId, part.text);
|
|
464
|
+
// The reply as it grows. Each update carries the part's whole text so
|
|
465
|
+
// far (newer builds add the delta too); only the assistant's, never
|
|
466
|
+
// the echo of your own prompt, which arrives as a text part as well.
|
|
467
|
+
if (this.roles.get(messageID) === "assistant") {
|
|
468
|
+
const delta = typeof props.delta === "string" ? props.delta : part.text.startsWith(prev) ? part.text.slice(prev.length) : "";
|
|
469
|
+
if (delta)
|
|
470
|
+
this.streamText(delta);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
else if (partType === "reasoning" && typeof part.text === "string") {
|
|
474
|
+
// its thinking, kept (not only streamed): the finished part replaces any earlier copy
|
|
475
|
+
if (this.roles.get(messageID) !== "user")
|
|
476
|
+
this.reasoningParts.set(`${messageID}:${String(part.id ?? "r")}`, part.text);
|
|
229
477
|
}
|
|
230
478
|
else if (partType === "tool") {
|
|
231
479
|
const state = (part.state ?? {});
|
|
232
|
-
|
|
480
|
+
const st = String(state.status ?? "");
|
|
481
|
+
const key = String(part.callID ?? part.id ?? "");
|
|
482
|
+
if ((st === "completed" || st === "error") && !this.toolsDone.has(key)) {
|
|
483
|
+
if (key)
|
|
484
|
+
this.toolsDone.add(key);
|
|
485
|
+
const input = state.input && typeof state.input === "object" ? state.input : undefined;
|
|
486
|
+
const output = typeof state.output === "string" ? state.output : "";
|
|
487
|
+
const error = st === "error" ? String(state.error ?? "failed") : "";
|
|
488
|
+
const tool = String(part.tool ?? "tool");
|
|
489
|
+
// a bash call that ran to the end "completed" even when its command failed: the exit code says which
|
|
490
|
+
const meta = state.metadata;
|
|
491
|
+
const exitCode = typeof meta?.exit === "number" ? meta.exit : undefined;
|
|
233
492
|
this.emit({
|
|
234
493
|
kind: "tool_call",
|
|
235
494
|
payload: {
|
|
236
|
-
tool
|
|
237
|
-
summary: String(state.title ??
|
|
495
|
+
tool,
|
|
496
|
+
summary: String(state.title ?? tool),
|
|
497
|
+
ok: st === "completed" && (exitCode === undefined || exitCode === 0),
|
|
498
|
+
...(exitCode !== undefined ? { exitCode } : {}),
|
|
499
|
+
...(tool === "bash" && typeof input?.command === "string" ? { command: input.command.slice(0, 2_000) } : {}),
|
|
500
|
+
...(input ? { input: Object.fromEntries(Object.entries(input).slice(0, 20).map(([k, v]) => [k, typeof v === "string" && v.length > 600 ? `${v.slice(0, 600)}\u2026` : v])) } : {}),
|
|
501
|
+
...(output.trim() ? { preview: output.length > 4000 ? `\u2026\n${output.slice(-4000)}` : output } : {}),
|
|
502
|
+
...(error ? { error: error.slice(0, 1500) } : {}),
|
|
238
503
|
},
|
|
239
504
|
});
|
|
505
|
+
// its todo list is its plan
|
|
506
|
+
if (tool === "todowrite" && Array.isArray(input?.todos))
|
|
507
|
+
this.emitPlan(input.todos);
|
|
240
508
|
}
|
|
241
509
|
}
|
|
242
510
|
else if (partType === "patch") {
|
|
@@ -255,8 +523,17 @@ export class OpenCodeAdapter extends AdapterBase {
|
|
|
255
523
|
const role = String(info.role ?? "");
|
|
256
524
|
if (role)
|
|
257
525
|
this.roles.set(messageID, role);
|
|
526
|
+
// the echo of our own prompt (briefing and all) is never emitted: don't keep it
|
|
527
|
+
if (role === "user")
|
|
528
|
+
this.textParts.delete(messageID);
|
|
258
529
|
const time = (info.time ?? {});
|
|
259
530
|
if (role === "assistant" && time.completed) {
|
|
531
|
+
const thought = [...this.reasoningParts.entries()].filter(([k]) => k.startsWith(`${messageID}:`)).map(([, v]) => v).join("\n").trim();
|
|
532
|
+
for (const k of [...this.reasoningParts.keys()])
|
|
533
|
+
if (k.startsWith(`${messageID}:`))
|
|
534
|
+
this.reasoningParts.delete(k);
|
|
535
|
+
if (thought)
|
|
536
|
+
this.emit({ kind: "message", payload: { text: thought, reasoning: true } });
|
|
260
537
|
const parts = this.textParts.get(messageID);
|
|
261
538
|
if (parts && parts.size) {
|
|
262
539
|
const text = [...parts.values()].join("").trim();
|
|
@@ -274,19 +551,209 @@ export class OpenCodeAdapter extends AdapterBase {
|
|
|
274
551
|
}
|
|
275
552
|
return;
|
|
276
553
|
}
|
|
277
|
-
if (
|
|
554
|
+
if (type === "todo.updated") {
|
|
555
|
+
if (props.sessionID && props.sessionID !== mySession)
|
|
556
|
+
return;
|
|
557
|
+
if (Array.isArray(props.todos))
|
|
558
|
+
this.emitPlan(props.todos);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (/^question(\.v2)?\.asked$/.test(type)) {
|
|
562
|
+
if (props.sessionID && props.sessionID !== mySession)
|
|
563
|
+
return;
|
|
564
|
+
this.askQuestion(props, type.includes(".v2"));
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
// Answered or dismissed somewhere else (opencode's own TUI, a timeout): fold the card.
|
|
568
|
+
if (/^question(\.v2)?\.(replied|rejected)$/.test(type)) {
|
|
569
|
+
const id = String(props.requestID ?? props.id ?? "");
|
|
570
|
+
if (id && this.questions.delete(`oc-q:${id}`)) {
|
|
571
|
+
this.emit({ kind: "status", payload: { state: "question_answered", requestId: `oc-q:${id}`, ...(type.endsWith("rejected") ? { answers: {} } : {}) } });
|
|
572
|
+
}
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
if (/^permission(\.v2)?\.asked$/.test(type)) {
|
|
278
576
|
if (props.sessionID && props.sessionID !== mySession)
|
|
279
577
|
return;
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
578
|
+
void this.askPermission(props, type.includes(".v2"));
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
reasoningParts = new Map();
|
|
582
|
+
toolsDone = new Set();
|
|
583
|
+
/** An image a tool returned (a file: URI, or data: base64 filed under .loom/attachments), as a path the thread can show. */
|
|
584
|
+
keepImage(uri, mime) {
|
|
585
|
+
try {
|
|
586
|
+
if (uri.startsWith("file://")) {
|
|
587
|
+
const abs = decodeURIComponent(uri.slice(7));
|
|
588
|
+
const rel = path.relative(this.projectDir, abs);
|
|
589
|
+
return [{ path: rel.startsWith("..") ? abs : rel.split(path.sep).join("/"), mime }];
|
|
590
|
+
}
|
|
591
|
+
const m = /^data:([^;,]+);base64,(.+)$/s.exec(uri);
|
|
592
|
+
if (!m)
|
|
593
|
+
return [];
|
|
594
|
+
const buf = Buffer.from(m[2], "base64");
|
|
595
|
+
if (!buf.length || buf.length > 12 * 1024 * 1024)
|
|
596
|
+
return [];
|
|
597
|
+
const ext = { "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif", "image/webp": "webp", "image/svg+xml": "svg" }[m[1]] ?? "png";
|
|
598
|
+
const rel = path.join(".loom", "attachments", `${createHash("sha1").update(buf).digest("hex").slice(0, 12)}.${ext}`);
|
|
599
|
+
const abs = path.join(this.projectDir, rel);
|
|
600
|
+
if (!fs.existsSync(abs)) {
|
|
601
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
602
|
+
fs.writeFileSync(abs, buf);
|
|
603
|
+
}
|
|
604
|
+
return [{ path: rel.split(path.sep).join("/"), mime }];
|
|
605
|
+
}
|
|
606
|
+
catch {
|
|
607
|
+
return [];
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
/** opencode's todos as Loom's plan checklist. */
|
|
611
|
+
emitPlan(todos) {
|
|
612
|
+
const plan = todos.map((t) => ({ step: String(t.content ?? t.text ?? ""),
|
|
613
|
+
status: t.status === "completed" ? "completed" : t.status === "in_progress" ? "inProgress" : "pending" })).filter((t) => t.step);
|
|
614
|
+
if (plan.length)
|
|
615
|
+
this.emit({ kind: "status", payload: { state: "plan_updated", plan } });
|
|
616
|
+
}
|
|
617
|
+
// ---- opencode asking you things ---------------------------------------------
|
|
618
|
+
/** Open questions by Loom request id → opencode's request, so an answer can find its way back. */
|
|
619
|
+
questions = new Map();
|
|
620
|
+
/**
|
|
621
|
+
* opencode's question tool: one or more questions, each with labelled
|
|
622
|
+
* options, maybe several picks, maybe your own words. The card in the thread
|
|
623
|
+
* answers it through respondToUserInput.
|
|
624
|
+
*/
|
|
625
|
+
askQuestion(props, _v2) {
|
|
626
|
+
const id = String(props.id ?? "");
|
|
627
|
+
const list = Array.isArray(props.questions) ? props.questions : [];
|
|
628
|
+
if (!id || !list.length)
|
|
629
|
+
return;
|
|
630
|
+
const questions = list.map((q, i) => ({
|
|
631
|
+
id: String(i),
|
|
632
|
+
header: String(q.header ?? "Question").slice(0, 40),
|
|
633
|
+
question: String(q.question ?? ""),
|
|
634
|
+
options: (Array.isArray(q.options) ? q.options : []).map((o) => typeof o === "string" ? { label: o, description: "" } : { label: String(o.label ?? ""), description: String(o.description ?? "") }),
|
|
635
|
+
multiSelect: q.multiple === true,
|
|
636
|
+
allowCustomAnswer: q.custom !== false,
|
|
637
|
+
}));
|
|
638
|
+
const requestId = `oc-q:${id}`;
|
|
639
|
+
this.questions.set(requestId, { id, sid: String(props.sessionID ?? this.activeSid ?? this.sessionId ?? ""), count: questions.length });
|
|
640
|
+
this.emit({
|
|
641
|
+
kind: "needs_input",
|
|
642
|
+
payload: { question: questions.map((q) => q.question).join("\n").slice(0, 500), questions, requestId, responseMode: "tool" },
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
/** Answer an open opencode question. answers: { [questionIndex]: label | labels }; none at all dismisses it. */
|
|
646
|
+
async respondToUserInput(_chat, requestId, answers) {
|
|
647
|
+
const open = this.questions.get(requestId);
|
|
648
|
+
if (!open || !this.baseUrl)
|
|
649
|
+
throw new Error(`no open opencode question "${requestId}"`);
|
|
650
|
+
const picks = Array.from({ length: open.count }, (_v, i) => {
|
|
651
|
+
const a = answers[String(i)];
|
|
652
|
+
return (Array.isArray(a) ? a : a == null || a === "" ? [] : [a]).map((x) => String(x));
|
|
653
|
+
});
|
|
654
|
+
const dismiss = picks.every((p) => !p.length);
|
|
655
|
+
// the session-scoped route on current builds, the global one on older
|
|
656
|
+
const routes = this.modernApi ? [`${this.baseUrl}/question/${encodeURIComponent(open.id)}/${dismiss ? "reject" : "reply"}`] : [
|
|
657
|
+
`${this.baseUrl}/api/session/${encodeURIComponent(open.sid)}/question/${encodeURIComponent(open.id)}/${dismiss ? "reject" : "reply"}`,
|
|
658
|
+
`${this.baseUrl}/question/${encodeURIComponent(open.id)}/${dismiss ? "reject" : "reply"}`,
|
|
659
|
+
];
|
|
660
|
+
let last = "";
|
|
661
|
+
for (const url of routes) {
|
|
662
|
+
const res = await fetch(url, { method: "POST", headers: { "content-type": "application/json" },
|
|
663
|
+
body: JSON.stringify(dismiss ? {} : { answers: picks }), signal: AbortSignal.timeout(15_000) }).catch((e) => ({ ok: false, status: 0, text: async () => e.message }));
|
|
664
|
+
if (res.ok) {
|
|
665
|
+
this.questions.delete(requestId);
|
|
666
|
+
this.emit({ kind: "status", payload: { state: "question_answered", requestId, answers } });
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
last = `${res.status} ${(await res.text().catch(() => "")).slice(0, 160)}`;
|
|
670
|
+
if (res.status !== 404)
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
throw new Error(`opencode didn't take the answer (${last})`);
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* opencode asking to run a command, edit a file, fetch a URL: the same
|
|
677
|
+
* approval card Loom shows for any agent, and opencode hears once / always / no.
|
|
678
|
+
*/
|
|
679
|
+
async askPermission(props, v2) {
|
|
680
|
+
const id = String(props.id ?? "");
|
|
681
|
+
if (!id || !this.baseUrl)
|
|
682
|
+
return;
|
|
683
|
+
const sid = String(props.sessionID ?? this.activeSid ?? this.sessionId ?? "");
|
|
684
|
+
const tool = String((v2 ? props.action : props.permission) ?? "permission");
|
|
685
|
+
const targets = (Array.isArray(v2 ? props.resources : props.patterns) ? (v2 ? props.resources : props.patterns) : []).map(String);
|
|
686
|
+
const meta = (props.metadata ?? {});
|
|
687
|
+
const summary = String(meta.command ?? meta.description ?? meta.filePath ?? meta.url ?? (targets.length ? `${tool} ${targets.join(", ")}` : tool)).slice(0, 300);
|
|
688
|
+
const decision = await requestApproval({
|
|
689
|
+
project: this.options.loomProject ?? "",
|
|
690
|
+
agent: this.id, tool, input: { ...meta, ...(targets.length ? { targets } : {}) }, summary, sessionOption: true,
|
|
691
|
+
});
|
|
692
|
+
const reply = decision.behavior !== "allow" ? "reject" : decision.scope === "session" ? "always" : "once";
|
|
693
|
+
const body = JSON.stringify({ reply, ...(decision.message ? { message: decision.message } : {}) });
|
|
694
|
+
for (const url of this.modernApi ? [`${this.baseUrl}/permission/${encodeURIComponent(id)}/reply`] : [`${this.baseUrl}/api/session/${encodeURIComponent(sid)}/permission/${encodeURIComponent(id)}/reply`, `${this.baseUrl}/permission/${encodeURIComponent(id)}/reply`]) {
|
|
695
|
+
const res = await fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body, signal: AbortSignal.timeout(15_000) }).catch(() => null);
|
|
696
|
+
if (res?.ok)
|
|
697
|
+
return;
|
|
698
|
+
if (res && res.status !== 404)
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
logbook.warn(`agent:${this.id}`, "opencode didn't take the permission reply", id);
|
|
702
|
+
}
|
|
703
|
+
/** opencode 1.18's tool and compaction events, for a session already known to be ours. */
|
|
704
|
+
handleSessionNext(type, props) {
|
|
705
|
+
const callId = String(props.callID ?? "");
|
|
706
|
+
if (type === "session.next.tool.called") {
|
|
707
|
+
const input = (props.input ?? {});
|
|
708
|
+
const title = String(input.description ?? input.command ?? input.filePath ?? input.path ?? input.pattern ?? props.tool ?? "tool");
|
|
709
|
+
this.toolCalls.set(callId, { tool: String(props.tool ?? "tool"), title: title.slice(0, 200), input });
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (type === "session.next.tool.success" || type === "session.next.tool.failed") {
|
|
713
|
+
const call = this.toolCalls.get(callId) ?? { tool: "tool", title: "tool" };
|
|
714
|
+
this.toolCalls.delete(callId);
|
|
715
|
+
// the same call also arrives as a message part on these builds: log it once
|
|
716
|
+
if (callId && this.toolsDone.has(callId))
|
|
717
|
+
return;
|
|
718
|
+
if (callId)
|
|
719
|
+
this.toolsDone.add(callId);
|
|
720
|
+
const err = (props.error ?? {});
|
|
721
|
+
// the result: text blocks are its output, file blocks with an image type are pictures it made or read
|
|
722
|
+
const content = Array.isArray(props.content) ? props.content : [];
|
|
723
|
+
const output = content.filter((c) => c.type === "text" && typeof c.text === "string").map((c) => String(c.text)).join("\n");
|
|
724
|
+
const images = content.filter((c) => c.type === "file" && /^image\//.test(String(c.mime ?? ""))).flatMap((c) => this.keepImage(String(c.uri ?? ""), String(c.mime)));
|
|
725
|
+
// bash "succeeds" when its command ran, failing or not: the exit code it reports says which
|
|
726
|
+
const structured = (props.structured ?? {});
|
|
727
|
+
const exitRaw = structured.exit ?? structured.metadata?.exit;
|
|
728
|
+
const exitCode = typeof exitRaw === "number" ? exitRaw : undefined;
|
|
729
|
+
const input = call.input ? Object.fromEntries(Object.entries(call.input).slice(0, 20).map(([k, v]) => [k, typeof v === "string" && v.length > 600 ? `${v.slice(0, 600)}\u2026` : v])) : undefined;
|
|
730
|
+
this.emit({
|
|
731
|
+
kind: "tool_call",
|
|
732
|
+
payload: { tool: call.tool, summary: call.title, ok: !type.endsWith("failed") && (exitCode === undefined || exitCode === 0),
|
|
733
|
+
...(exitCode !== undefined ? { exitCode } : {}),
|
|
734
|
+
...(call.tool === "bash" && typeof call.input?.command === "string" ? { command: String(call.input.command).slice(0, 2_000) } : {}),
|
|
735
|
+
...(input && Object.keys(input).length ? { input } : {}),
|
|
736
|
+
...(output.trim() ? { preview: output.length > 4000 ? `\u2026\n${output.slice(-4000)}` : output } : {}),
|
|
737
|
+
...(images.length ? { images } : {}),
|
|
738
|
+
...(type.endsWith("failed") ? { error: String(err.message ?? "failed").slice(0, 1500) } : {}) },
|
|
739
|
+
});
|
|
740
|
+
if (call.tool === "todowrite" && Array.isArray(call.input?.todos))
|
|
741
|
+
this.emitPlan(call.input.todos);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (type === "session.next.compaction.started") {
|
|
745
|
+
this.compactionReported = false;
|
|
746
|
+
this.emit({ kind: "status", payload: { state: "compacting", reason: String(props.reason ?? "auto") } });
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
if ((type === "session.next.compaction.ended" || type === "session.compacted") && !this.compactionReported) {
|
|
750
|
+
this.compactionReported = true;
|
|
751
|
+
this.emit({ kind: "status", payload: { state: "native_compacted", session: String(props.sessionID ?? this.activeSid ?? "") } });
|
|
285
752
|
}
|
|
286
753
|
}
|
|
287
754
|
/** All messages in the session, oldest first (info objects). */
|
|
288
755
|
async listMessages(sid) {
|
|
289
|
-
const res = await fetchJson(
|
|
756
|
+
const res = await fetchJson(this.api(`/session/${sid}/message`));
|
|
290
757
|
const data = (res.data ?? res);
|
|
291
758
|
const items = Array.isArray(data) ? data : [];
|
|
292
759
|
return items
|
|
@@ -301,9 +768,23 @@ export class OpenCodeAdapter extends AdapterBase {
|
|
|
301
768
|
* fact holds across two consecutive polls (nothing new started).
|
|
302
769
|
* `/wait` is tried first but returns 503 on 1.17.
|
|
303
770
|
*/
|
|
771
|
+
/** Is opencode still running this session? Null when this opencode can't say (older builds). */
|
|
772
|
+
async sessionActive(sid) {
|
|
773
|
+
const res = await fetchJson(this.modernApi ? this.api("/session/status") : this.api("/session/active")).catch(() => null);
|
|
774
|
+
const active = res ? (res.data ?? res) : null;
|
|
775
|
+
if (!active || typeof active !== "object" || Array.isArray(active))
|
|
776
|
+
return null;
|
|
777
|
+
if (!Object.prototype.hasOwnProperty.call(active, sid))
|
|
778
|
+
return false;
|
|
779
|
+
if (!this.modernApi)
|
|
780
|
+
return true;
|
|
781
|
+
const type = active[sid]?.type;
|
|
782
|
+
return type === "idle" ? false : type === "busy" || type === "retry" ? true : null;
|
|
783
|
+
}
|
|
304
784
|
async waitForTurn(sid, baseline, timeoutMs) {
|
|
305
785
|
try {
|
|
306
|
-
|
|
786
|
+
if (!this.modernApi)
|
|
787
|
+
await fetchJson(`${this.baseUrl}/api/session/${sid}/wait`, { method: "POST", headers: { "content-type": "application/json" }, body: "{}" }, timeoutMs);
|
|
307
788
|
}
|
|
308
789
|
catch {
|
|
309
790
|
// 503 "not available yet" on 1.17 — fall through to polling.
|
|
@@ -311,6 +792,13 @@ export class OpenCodeAdapter extends AdapterBase {
|
|
|
311
792
|
const deadline = Date.now() + timeoutMs;
|
|
312
793
|
let stableId = null;
|
|
313
794
|
while (Date.now() < deadline) {
|
|
795
|
+
// Interrupted: done as soon as opencode stops running it, answer or not
|
|
796
|
+
// (one that never answered would otherwise be waited on for the hour).
|
|
797
|
+
if (this.interrupted && (await this.sessionActive(sid)) !== true) {
|
|
798
|
+
const after = await this.listMessages(sid).catch(() => []);
|
|
799
|
+
const last = after.filter((m) => (m.type ?? m.role) === "assistant" && !baseline.has(String(m.id))).at(-1);
|
|
800
|
+
return last ?? null;
|
|
801
|
+
}
|
|
314
802
|
const messages = await this.listMessages(sid).catch(() => []);
|
|
315
803
|
const newest = messages[messages.length - 1];
|
|
316
804
|
const newAssistants = messages.filter((m) => (m.type ?? m.role) === "assistant" &&
|
|
@@ -321,88 +809,372 @@ export class OpenCodeAdapter extends AdapterBase {
|
|
|
321
809
|
newest.time?.completed &&
|
|
322
810
|
newAssistants.length > 0;
|
|
323
811
|
if (turnLooksDone) {
|
|
324
|
-
// Errors are terminal immediately
|
|
325
|
-
//
|
|
812
|
+
// Errors are terminal immediately. A finished *step* is not a finished
|
|
813
|
+
// turn: one that ended in tool calls is followed by another, and a slow
|
|
814
|
+
// model can take longer than a poll to start it — so a session opencode
|
|
815
|
+
// still lists as active, or a step that finished on tool calls, keeps
|
|
816
|
+
// waiting. Otherwise require stability across two polls.
|
|
326
817
|
if (newest.finish === "error" || newest.error)
|
|
327
818
|
return newest;
|
|
328
|
-
|
|
819
|
+
const midTurn = /tool/i.test(String(newest.finish ?? "")) || (await this.sessionActive(sid)) === true;
|
|
820
|
+
if (midTurn)
|
|
821
|
+
stableId = null;
|
|
822
|
+
else if (stableId === String(newest.id))
|
|
329
823
|
return newest;
|
|
330
|
-
|
|
824
|
+
else
|
|
825
|
+
stableId = String(newest.id);
|
|
331
826
|
}
|
|
332
827
|
else {
|
|
333
828
|
stableId = null;
|
|
334
829
|
}
|
|
335
|
-
await new Promise((r) => setTimeout(r, 3000));
|
|
830
|
+
await new Promise((r) => setTimeout(r, typeof this.options.pollMs === "number" ? this.options.pollMs : 3000));
|
|
336
831
|
}
|
|
337
832
|
return null;
|
|
338
833
|
}
|
|
339
834
|
async send(input) {
|
|
835
|
+
if (input.continuity)
|
|
836
|
+
return this.sendContinuity(input);
|
|
837
|
+
this.cancelIdle();
|
|
340
838
|
if (!this.started)
|
|
341
839
|
await this.start();
|
|
342
840
|
if (this._busy)
|
|
343
841
|
throw new Error(`opencode agent "${this.id}" is busy`);
|
|
344
842
|
this._busy = true;
|
|
843
|
+
this.trimTurnState();
|
|
844
|
+
this.interrupted = false;
|
|
345
845
|
const started = Date.now();
|
|
346
846
|
const timeoutMs = 60 * 60 * 1000;
|
|
347
847
|
try {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const
|
|
368
|
-
if (
|
|
369
|
-
|
|
370
|
-
this.emit({
|
|
371
|
-
kind: "error",
|
|
372
|
-
payload: {
|
|
373
|
-
message: String(err.message ?? "opencode turn failed").slice(0, 500),
|
|
374
|
-
},
|
|
375
|
-
});
|
|
848
|
+
// One retry, in a fresh session, when opencode refuses the session's own
|
|
849
|
+
// history: a single bad picture in it (media that isn't valid base64)
|
|
850
|
+
// is replayed on every later turn and would lock this agent out for good.
|
|
851
|
+
for (let attempt = 0;; attempt++) {
|
|
852
|
+
const sid = await this.ensureSession();
|
|
853
|
+
const baseline = new Set((await this.listMessages(sid).catch(() => [])).map((m) => String(m.id)));
|
|
854
|
+
// No per-prompt system field in the API, so the handoff briefing rides in
|
|
855
|
+
// the prompt — framed as an unmissable authoritative block (frameBriefing)
|
|
856
|
+
// rather than a loose preamble.
|
|
857
|
+
const text = input.briefing ? `${frameBriefing(input.briefing)}\n\n${input.text}` : input.text;
|
|
858
|
+
const admission = await fetch(this.api(`/session/${sid}/${this.modernApi ? "prompt_async" : "prompt"}`), {
|
|
859
|
+
method: "POST",
|
|
860
|
+
headers: { "content-type": "application/json" },
|
|
861
|
+
body: JSON.stringify(this.promptBody(text)),
|
|
862
|
+
signal: AbortSignal.timeout(30_000),
|
|
863
|
+
});
|
|
864
|
+
if (!admission.ok)
|
|
865
|
+
throw new Error(`opencode prompt failed (${admission.status}): ${(await admission.text()).slice(0, 500)}`);
|
|
866
|
+
await admission.arrayBuffer();
|
|
867
|
+
const turn = await this.waitForTurn(sid, baseline, timeoutMs);
|
|
868
|
+
if (!turn && this.interrupted) {
|
|
869
|
+
this.emit({ kind: "status", payload: { state: "interrupted" } });
|
|
376
870
|
}
|
|
377
|
-
else if (!
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
871
|
+
else if (!turn) {
|
|
872
|
+
this.emit({ kind: "error", payload: { message: "turn timed out waiting for opencode" } });
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
const turnId = String(turn.id);
|
|
876
|
+
// Fetch the full message: parts (in case SSE missed them) + errors.
|
|
877
|
+
const detail = await this.messageDetail(sid, turnId);
|
|
878
|
+
const info = (detail?.data ?? detail ?? turn) ?? turn;
|
|
879
|
+
if (info.finish === "error" || info.error) {
|
|
880
|
+
const err = (info.error ?? {});
|
|
881
|
+
const message = turnError(err);
|
|
882
|
+
if (attempt === 0 && POISONED_HISTORY.test(message)) {
|
|
883
|
+
this.sessionId = undefined;
|
|
884
|
+
this.emit({ kind: "status", payload: { state: "session_reset", reason: `opencode refused this session's history (${message}); carrying on in a fresh session` } });
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
this.emit({ kind: "error", payload: { message } });
|
|
387
888
|
}
|
|
889
|
+
else if (!this.emittedText.has(turnId)) {
|
|
890
|
+
const content = Array.isArray(info.content) ? info.content : [];
|
|
891
|
+
const text = content
|
|
892
|
+
.filter((p) => p.type === "text" && typeof p.text === "string")
|
|
893
|
+
.map((p) => String(p.text))
|
|
894
|
+
.join("")
|
|
895
|
+
.trim();
|
|
896
|
+
if (text) {
|
|
897
|
+
this.emit({ kind: "message", payload: { text } });
|
|
898
|
+
this.emittedText.add(turnId);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
const cost = Number(info.cost ?? 0);
|
|
902
|
+
if (cost > 0) {
|
|
903
|
+
this.emit({ kind: "status", payload: { state: "turn_cost", costUsd: cost } });
|
|
904
|
+
}
|
|
905
|
+
// OpenCode assistant messages carry token usage; capture it (cache
|
|
906
|
+
// reads/writes count as input) so it isn't dropped.
|
|
907
|
+
const tk = (info.tokens ?? {});
|
|
908
|
+
const cache = (tk.cache ?? {});
|
|
909
|
+
this.lastUsage = {
|
|
910
|
+
input: (tk.input ?? 0) + (cache.read ?? 0) + (cache.write ?? 0),
|
|
911
|
+
output: (tk.output ?? 0) + (tk.reasoning ?? 0),
|
|
912
|
+
cached: cache.read ?? 0,
|
|
913
|
+
reasoning: tk.reasoning ?? 0,
|
|
914
|
+
};
|
|
915
|
+
const mid = info.modelID;
|
|
916
|
+
const pid = info.providerID;
|
|
917
|
+
if (typeof mid === "string" && mid)
|
|
918
|
+
this.lastModel = typeof pid === "string" && pid ? `${pid}/${mid}` : mid;
|
|
388
919
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
920
|
+
break;
|
|
921
|
+
}
|
|
922
|
+
this.emit({
|
|
923
|
+
kind: "run_complete",
|
|
924
|
+
payload: {
|
|
925
|
+
durationMs: Date.now() - started,
|
|
926
|
+
...((this.lastModel ?? this.options.model) ? { model: this.lastModel ?? this.options.model } : {}),
|
|
927
|
+
...(this.lastUsage ? usagePayload(this.lastUsage) : {}),
|
|
928
|
+
},
|
|
929
|
+
});
|
|
930
|
+
this.lastUsage = null;
|
|
931
|
+
this.lastModel = null;
|
|
932
|
+
}
|
|
933
|
+
finally {
|
|
934
|
+
this._busy = false;
|
|
935
|
+
this.armIdle();
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* A continuity turn (see the header): the binding's session, the admission as
|
|
940
|
+
* acceptance, and the session leaving `/api/session/active` as the end.
|
|
941
|
+
* Failures before the prompt is admitted are NativeDispatchRejected — nothing
|
|
942
|
+
* reached opencode — and a lost acknowledgement is never retried.
|
|
943
|
+
*/
|
|
944
|
+
async sendContinuity(input) {
|
|
945
|
+
const c = input.continuity;
|
|
946
|
+
if (this._busy)
|
|
947
|
+
throw new NativeDispatchRejected(`opencode agent "${this.id}" is busy`);
|
|
948
|
+
this.cancelIdle();
|
|
949
|
+
this._busy = true;
|
|
950
|
+
this.trimTurnState();
|
|
951
|
+
this.interrupted = false;
|
|
952
|
+
this.beginContinuity(input);
|
|
953
|
+
const started = Date.now();
|
|
954
|
+
let admitted = false;
|
|
955
|
+
try {
|
|
956
|
+
if (!this.started) {
|
|
957
|
+
try {
|
|
958
|
+
await this.start();
|
|
959
|
+
}
|
|
960
|
+
catch (err) {
|
|
961
|
+
throw new NativeDispatchRejected(`opencode didn't start: ${err.message}`);
|
|
392
962
|
}
|
|
393
963
|
}
|
|
394
|
-
|
|
964
|
+
const sid = await this.continuitySession(c.nativeSessionId, input.model);
|
|
965
|
+
this.activeSid = sid;
|
|
966
|
+
this.emit({ kind: "status", payload: { state: "turn_started", session: sid } });
|
|
967
|
+
if (this.interrupted)
|
|
968
|
+
throw new NativeDispatchRejected("interrupted before the turn started");
|
|
969
|
+
const baseline = new Set((await this.listMessages(sid).catch(() => [])).map((m) => String(m.id)));
|
|
970
|
+
const text = [c.context, input.briefing, input.text].filter(Boolean).join("\n\n");
|
|
971
|
+
let res;
|
|
972
|
+
try {
|
|
973
|
+
res = await fetch(this.api(`/session/${encodeURIComponent(sid)}/${this.modernApi ? "prompt_async" : "prompt"}`), {
|
|
974
|
+
method: "POST",
|
|
975
|
+
headers: { "content-type": "application/json" },
|
|
976
|
+
body: JSON.stringify(this.modernApi ? { ...this.promptBody(text), ...(input.model ? { model: { providerID: parseModelRef(input.model)?.providerID, modelID: parseModelRef(input.model)?.id } } : {}) } : { prompt: promptWithFiles(text, this.projectDir) }),
|
|
977
|
+
signal: AbortSignal.timeout(30_000),
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
catch (err) {
|
|
981
|
+
// The request may or may not have reached the server: unknown, not rejected.
|
|
982
|
+
throw new Error(`opencode prompt request failed: ${err.message}; native outcome is unknown`);
|
|
983
|
+
}
|
|
984
|
+
if (!res.ok) {
|
|
985
|
+
const body = await res.text().catch(() => "");
|
|
986
|
+
throw new NativeDispatchRejected(`opencode refused the prompt (${res.status}): ${body.slice(0, 300)}`);
|
|
987
|
+
}
|
|
988
|
+
if (this.modernApi && res.status !== 204)
|
|
989
|
+
throw new Error(`opencode prompt returned unexpected admission status ${res.status}; native outcome is unknown`);
|
|
990
|
+
const ack = (await res.json().catch(() => ({}))).data;
|
|
991
|
+
admitted = true;
|
|
992
|
+
this.emit({ kind: "status", payload: { state: "native_turn_accepted", session: sid, ...(ack?.id ? { messageId: String(ack.id) } : {}) } });
|
|
993
|
+
const idle = await this.waitIdle(sid, baseline, this.turnTimeoutMs());
|
|
994
|
+
if (!idle) {
|
|
995
|
+
this.emit({ kind: "error", payload: { message: "opencode was still working when the turn timed out" } });
|
|
996
|
+
throw new NativeQuiescenceUnknown("opencode still lists the session as active; quiescence unknown");
|
|
997
|
+
}
|
|
998
|
+
if (this.interrupted) {
|
|
999
|
+
this.emit({ kind: "status", payload: { state: "interrupted" } });
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
const messages = await this.listMessages(sid).catch(() => []);
|
|
1003
|
+
const turn = [...messages].reverse().find((m) => (m.type ?? m.role) === "assistant" && !baseline.has(String(m.id)));
|
|
1004
|
+
if (!turn) {
|
|
1005
|
+
const message = "opencode finished without a reply";
|
|
1006
|
+
this.emit({ kind: "error", payload: { message } });
|
|
1007
|
+
throw new Error(message);
|
|
1008
|
+
}
|
|
1009
|
+
const failed = await this.reportTurn(sid, turn);
|
|
1010
|
+
if (failed)
|
|
1011
|
+
throw new Error(`opencode reported a failed turn: ${failed}`);
|
|
1012
|
+
this.emit({
|
|
1013
|
+
kind: "run_complete",
|
|
1014
|
+
payload: {
|
|
1015
|
+
durationMs: Date.now() - started,
|
|
1016
|
+
session: sid,
|
|
1017
|
+
...((this.lastModel ?? this.options.model) ? { model: this.lastModel ?? this.options.model } : {}),
|
|
1018
|
+
...(this.lastUsage ? usagePayload(this.lastUsage) : {}),
|
|
1019
|
+
},
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
catch (err) {
|
|
1023
|
+
if (!admitted && !(err instanceof NativeSessionMissing) && !(err instanceof NativeDispatchRejected) && !/native outcome is unknown/.test(String(err.message))) {
|
|
1024
|
+
throw new NativeDispatchRejected(err.message);
|
|
1025
|
+
}
|
|
1026
|
+
throw err;
|
|
395
1027
|
}
|
|
396
1028
|
finally {
|
|
1029
|
+
this.lastUsage = null;
|
|
1030
|
+
this.lastModel = null;
|
|
1031
|
+
this.activeSid = undefined;
|
|
397
1032
|
this._busy = false;
|
|
1033
|
+
this.endContinuity();
|
|
1034
|
+
this.armIdle();
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
turnTimeoutMs() {
|
|
1038
|
+
const t = this.options.turnTimeoutMs;
|
|
1039
|
+
return typeof t === "number" && t > 0 ? t : 60 * 60 * 1000;
|
|
1040
|
+
}
|
|
1041
|
+
/** The session a continuity turn runs on: its binding's, or a new one. Nothing is sent yet. */
|
|
1042
|
+
async continuitySession(bound, turnModel) {
|
|
1043
|
+
const want = turnModel ?? this.options.model;
|
|
1044
|
+
const ref = want ? parseModelRef(want) : null;
|
|
1045
|
+
if (bound) {
|
|
1046
|
+
let res;
|
|
1047
|
+
for (let attempt = 0;; attempt++) {
|
|
1048
|
+
try {
|
|
1049
|
+
res = await fetch(this.api(`/session/${encodeURIComponent(bound)}`), { signal: AbortSignal.timeout(15_000) });
|
|
1050
|
+
break;
|
|
1051
|
+
}
|
|
1052
|
+
catch (err) {
|
|
1053
|
+
// A stale keep-alive socket can reset between native turns. Only
|
|
1054
|
+
// this read-only preflight is retried; prompt admission never is.
|
|
1055
|
+
if (attempt === 0)
|
|
1056
|
+
continue;
|
|
1057
|
+
throw new NativeDispatchRejected(`opencode server unreachable: ${err.message}`);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
if (res.status === 404)
|
|
1061
|
+
throw new NativeSessionMissing(`opencode no longer has session ${bound}`);
|
|
1062
|
+
if (!res.ok)
|
|
1063
|
+
throw new NativeDispatchRejected(`opencode answered ${res.status} for session ${bound}`);
|
|
1064
|
+
const info = ((await res.json().catch(() => ({}))).data ?? {});
|
|
1065
|
+
const cur = (info.model ?? {});
|
|
1066
|
+
// A model change keeps the session, as it does for the other harnesses.
|
|
1067
|
+
if (!this.modernApi && ref && (cur.providerID !== ref.providerID || cur.id !== ref.id)) {
|
|
1068
|
+
try {
|
|
1069
|
+
await fetchJson(`${this.baseUrl}/api/session/${encodeURIComponent(bound)}/model`, {
|
|
1070
|
+
method: "POST",
|
|
1071
|
+
headers: { "content-type": "application/json" },
|
|
1072
|
+
body: JSON.stringify({ model: ref }),
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
catch (err) {
|
|
1076
|
+
throw new NativeDispatchRejected(`opencode couldn't switch the session to ${want}: ${err.message}`);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
return bound;
|
|
398
1080
|
}
|
|
1081
|
+
const body = {};
|
|
1082
|
+
if (ref)
|
|
1083
|
+
body.model = ref;
|
|
1084
|
+
if (this.options.agent)
|
|
1085
|
+
body.agent = this.options.agent;
|
|
1086
|
+
try {
|
|
1087
|
+
const created = await fetchJson(this.api("/session"), {
|
|
1088
|
+
method: "POST",
|
|
1089
|
+
headers: { "content-type": "application/json" },
|
|
1090
|
+
body: JSON.stringify(this.modernApi ? {} : body),
|
|
1091
|
+
});
|
|
1092
|
+
const id = String((created.data ?? created).id ?? "");
|
|
1093
|
+
if (!id)
|
|
1094
|
+
throw new Error("no session id in the reply");
|
|
1095
|
+
return id;
|
|
1096
|
+
}
|
|
1097
|
+
catch (err) {
|
|
1098
|
+
throw new NativeDispatchRejected(`opencode couldn't create a session: ${err.message}`);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Wait until opencode no longer lists the session as active. Before it has
|
|
1103
|
+
* been seen running, an idle session only counts as done once a new
|
|
1104
|
+
* completed assistant message exists — the prompt may not have started yet.
|
|
1105
|
+
*/
|
|
1106
|
+
async waitIdle(sid, baseline, timeoutMs) {
|
|
1107
|
+
const pollMs = typeof this.options.pollMs === "number" ? this.options.pollMs : 500;
|
|
1108
|
+
const deadline = Date.now() + timeoutMs;
|
|
1109
|
+
let sawRunning = false;
|
|
1110
|
+
while (Date.now() < deadline) {
|
|
1111
|
+
const active = await this.sessionActive(sid);
|
|
1112
|
+
if (active === true)
|
|
1113
|
+
sawRunning = true;
|
|
1114
|
+
else if (active === false) {
|
|
1115
|
+
if (sawRunning || this.interrupted)
|
|
1116
|
+
return true;
|
|
1117
|
+
const messages = await this.listMessages(sid).catch(() => []);
|
|
1118
|
+
if (messages.some((m) => (m.type ?? m.role) === "assistant" && !baseline.has(String(m.id)) && m.time?.completed))
|
|
1119
|
+
return true;
|
|
1120
|
+
}
|
|
1121
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
1122
|
+
}
|
|
1123
|
+
return false;
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Emit a finished assistant message's text (unless the stream already did),
|
|
1127
|
+
* its cost, and stash its usage and model for run_complete. Returns the
|
|
1128
|
+
* error message when opencode says the turn failed.
|
|
1129
|
+
*/
|
|
1130
|
+
async reportTurn(sid, turn) {
|
|
1131
|
+
const turnId = String(turn.id);
|
|
1132
|
+
const detail = await this.messageDetail(sid, turnId);
|
|
1133
|
+
const info = (detail?.data ?? detail ?? turn) ?? turn;
|
|
1134
|
+
if (info.finish === "error" || info.error) {
|
|
1135
|
+
const err = (info.error ?? {});
|
|
1136
|
+
const message = turnError(err);
|
|
1137
|
+
this.emit({ kind: "error", payload: { message } });
|
|
1138
|
+
return message;
|
|
1139
|
+
}
|
|
1140
|
+
if (!this.emittedText.has(turnId)) {
|
|
1141
|
+
const content = Array.isArray(info.content) ? info.content : [];
|
|
1142
|
+
const text = content
|
|
1143
|
+
.filter((p) => p.type === "text" && typeof p.text === "string")
|
|
1144
|
+
.map((p) => String(p.text))
|
|
1145
|
+
.join("")
|
|
1146
|
+
.trim();
|
|
1147
|
+
if (text) {
|
|
1148
|
+
this.emit({ kind: "message", payload: { text } });
|
|
1149
|
+
this.emittedText.add(turnId);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
const cost = Number(info.cost ?? 0);
|
|
1153
|
+
if (cost > 0)
|
|
1154
|
+
this.emit({ kind: "status", payload: { state: "turn_cost", costUsd: cost } });
|
|
1155
|
+
const tk = (info.tokens ?? {});
|
|
1156
|
+
const cache = (tk.cache ?? {});
|
|
1157
|
+
this.lastUsage = {
|
|
1158
|
+
input: (tk.input ?? 0) + (cache.read ?? 0) + (cache.write ?? 0),
|
|
1159
|
+
output: (tk.output ?? 0) + (tk.reasoning ?? 0),
|
|
1160
|
+
cached: cache.read ?? 0,
|
|
1161
|
+
reasoning: tk.reasoning ?? 0,
|
|
1162
|
+
};
|
|
1163
|
+
const mid = info.modelID, pid = info.providerID;
|
|
1164
|
+
const model = (info.model ?? {});
|
|
1165
|
+
if (typeof mid === "string" && mid)
|
|
1166
|
+
this.lastModel = typeof pid === "string" && pid ? `${pid}/${mid}` : mid;
|
|
1167
|
+
else if (typeof model.id === "string")
|
|
1168
|
+
this.lastModel = typeof model.providerID === "string" ? `${model.providerID}/${model.id}` : model.id;
|
|
1169
|
+
return null;
|
|
399
1170
|
}
|
|
400
1171
|
async interrupt() {
|
|
401
|
-
|
|
1172
|
+
this.interrupted = true;
|
|
1173
|
+
const sid = this.activeSid ?? this.sessionId;
|
|
402
1174
|
if (!sid || !this.baseUrl)
|
|
403
1175
|
return;
|
|
404
1176
|
try {
|
|
405
|
-
await fetchJson(
|
|
1177
|
+
await fetchJson(this.api(`/session/${sid}/${this.modernApi ? "abort" : "interrupt"}`), {
|
|
406
1178
|
method: "POST",
|
|
407
1179
|
headers: { "content-type": "application/json" },
|
|
408
1180
|
body: "{}",
|
|
@@ -413,11 +1185,15 @@ export class OpenCodeAdapter extends AdapterBase {
|
|
|
413
1185
|
}
|
|
414
1186
|
}
|
|
415
1187
|
async stop() {
|
|
1188
|
+
this.cancelIdle();
|
|
416
1189
|
this.started = false;
|
|
417
1190
|
this.sseAbort?.abort();
|
|
418
1191
|
this.sseAbort = null;
|
|
419
1192
|
if (this.child) {
|
|
420
|
-
|
|
1193
|
+
if (process.platform === "win32")
|
|
1194
|
+
await stopWindowsTree(this.child);
|
|
1195
|
+
else
|
|
1196
|
+
await interruptProcess(this.child, true);
|
|
421
1197
|
this.child = null;
|
|
422
1198
|
}
|
|
423
1199
|
this.emit({ kind: "status", payload: { state: "stopped" } });
|