@aiwg/cli 2026.10.0 → 2026.10.3
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/agentic/code/providers/capability-matrix.yaml +270 -0
- package/agentic/code/providers/model-capabilities.v1.json +3091 -24
- package/bin/aiwg.mjs +86 -21
- package/dist/src/a2a/codecs.js +9 -0
- package/dist/src/artifacts/corpus-tools/vision-extract.js +105 -15
- package/dist/src/artifacts/repair.js +9 -4
- package/dist/src/channel/manager.mjs +59 -10
- package/dist/src/cli/child-process-result.js +40 -0
- package/dist/src/cli/env.js +38 -16
- package/dist/src/cli/flag-parsing.js +236 -0
- package/dist/src/cli/handlers/decision.js +297 -2
- package/dist/src/cli/handlers/diagnose.js +9 -2
- package/dist/src/cli/handlers/feedback.js +14 -5
- package/dist/src/cli/handlers/init.js +245 -30
- package/dist/src/cli/handlers/mc.js +23 -0
- package/dist/src/cli/handlers/models.js +69 -6
- package/dist/src/cli/handlers/ralph.js +3 -0
- package/dist/src/cli/handlers/refresh.js +16 -0
- package/dist/src/cli/handlers/regenerate.js +14 -0
- package/dist/src/cli/handlers/runtime-info.js +24 -9
- package/dist/src/cli/handlers/script-runner.js +16 -6
- package/dist/src/cli/handlers/session.js +396 -62
- package/dist/src/cli/handlers/sessions.js +146 -40
- package/dist/src/cli/handlers/setup-manifest.js +439 -39
- package/dist/src/cli/handlers/setup.js +139 -39
- package/dist/src/cli/handlers/subcommands.js +3 -1
- package/dist/src/cli/handlers/team.js +18 -8
- package/dist/src/cli/handlers/use.js +13 -0
- package/dist/src/cli/handlers/utilities.js +106 -23
- package/dist/src/cli/prompt-utils.js +72 -0
- package/dist/src/cli/provider-resolution.js +11 -0
- package/dist/src/config/aiwg-config.js +268 -4
- package/dist/src/config/cli.js +146 -19
- package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
- package/dist/src/config/project-artifacts-health.mjs +14 -3
- package/dist/src/config/user-config.js +36 -10
- package/dist/src/decision/adapters/jev.js +80 -28
- package/dist/src/decision/ask.js +352 -0
- package/dist/src/decision/driver.js +4 -0
- package/dist/src/decision/entry.js +38 -6
- package/dist/src/decision/heldout/collector.js +10 -1
- package/dist/src/decision/index.js +3 -0
- package/dist/src/decision/interop.js +60 -8
- package/dist/src/decision/jev-credentials.js +121 -0
- package/dist/src/decision/ledger.js +839 -0
- package/dist/src/decision/validate.js +6 -3
- package/dist/src/effects/dispatch-verifier.js +252 -0
- package/dist/src/effects/index.js +1 -0
- package/dist/src/features/installer.js +2 -2
- package/dist/src/flow/graph-metadata.js +5 -0
- package/dist/src/gates/providers/loader.js +3 -1
- package/dist/src/installation/manager-command.mjs +1 -1
- package/dist/src/installation/manager.mjs +17 -2
- package/dist/src/lint/runner.js +2 -2
- package/dist/src/mcp/adapters/codex-runtime.js +360 -56
- package/dist/src/mcp/cli.mjs +587 -77
- package/dist/src/mcp/config-file.mjs +279 -0
- package/dist/src/mcp/config-layers.mjs +348 -0
- package/dist/src/mcp/credentials.mjs +484 -0
- package/dist/src/mcp/grok-build-config.mjs +16 -26
- package/dist/src/mcp/helpers.mjs +106 -2
- package/dist/src/mcp/omp-config.mjs +50 -29
- package/dist/src/mcp/profiles.js +97 -23
- package/dist/src/mcp/profiles.mjs +99 -27
- package/dist/src/mcp/registry.js +264 -57
- package/dist/src/mcp/registry.mjs +198 -45
- package/dist/src/mcp/server.mjs +403 -20
- package/dist/src/mcp/toml-string.mjs +9 -0
- package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
- package/dist/src/mcp/tool-filters.mjs +357 -0
- package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
- package/dist/src/mcp/tools/decision.mjs +58 -0
- package/dist/src/mcp/tools/orchestration.mjs +38 -0
- package/dist/src/mcp/tools/subsystems.mjs +39 -8
- package/dist/src/mcp/tools/worker-invoke.mjs +1539 -0
- package/dist/src/mcp/tools/worker-shell-control.mjs +258 -0
- package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
- package/dist/src/memory/project-registry.js +2 -2
- package/dist/src/mission-protocol/codecs.js +639 -1
- package/dist/src/mission-protocol/types.js +110 -0
- package/dist/src/mission-protocol/worker-status.js +2 -0
- package/dist/src/models/config-loader.js +70 -13
- package/dist/src/models/grok-build-models.js +93 -41
- package/dist/src/models/model-capabilities.v1.json +3091 -24
- package/dist/src/models/model-discovery.js +862 -63
- package/dist/src/models/provider-policy.js +625 -11
- package/dist/src/models/qualification-digest.mjs +17 -0
- package/dist/src/models/resolver.js +259 -0
- package/dist/src/models/worker-direct-readiness.js +529 -0
- package/dist/src/models/worker-dispatch-offer.js +169 -0
- package/dist/src/models/worker-health-readiness.js +131 -0
- package/dist/src/models/worker-installation.mjs +879 -0
- package/dist/src/models/wrapper-route.js +18 -0
- package/dist/src/ops/registry.js +9 -14
- package/dist/src/plugins/standalone-packager.js +4 -3
- package/dist/src/process/bounded-child-process.mjs +387 -0
- package/dist/src/process/cli-command.mjs +75 -0
- package/dist/src/providers/capability-matrix.js +8 -0
- package/dist/src/providers/capability-matrix.yaml +270 -0
- package/dist/src/providers/grok-build-headless.js +43 -21
- package/dist/src/providers/provider-definitions.js +9 -1
- package/dist/src/providers/provider-definitions.mjs +9 -2
- package/dist/src/serve/executor-registry.js +16 -24
- package/dist/src/serve/fleet-mission-conductor.js +9 -0
- package/dist/src/serve/mission-conductor.js +9 -0
- package/dist/src/serve/shared-host-scheduler.js +2 -0
- package/dist/src/serve/worker-invocation.js +538 -0
- package/dist/src/serve/worker-stdin-frame.mjs +107 -0
- package/dist/src/sessions/adapter-registry.js +45 -0
- package/dist/src/sessions/adapters/generic.js +37 -3
- package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
- package/dist/src/sessions/adapters/grokbot.js +423 -24
- package/dist/src/sessions/adapters/muse-workspace-scan.js +108 -0
- package/dist/src/sessions/adapters/muse.js +68 -29
- package/dist/src/sessions/adapters/pi.js +1 -0
- package/dist/src/sessions/batch-contracts.js +13 -1
- package/dist/src/sessions/batch-import.js +23 -42
- package/dist/src/sessions/candidates.js +47 -11
- package/dist/src/sessions/contracts.js +35 -0
- package/dist/src/sessions/coverage-preflight.js +134 -0
- package/dist/src/sessions/discovery.js +3 -0
- package/dist/src/sessions/grok-build-cli.js +67 -5
- package/dist/src/sessions/importer.js +24 -8
- package/dist/src/sessions/index.js +2 -0
- package/dist/src/sessions/repository.js +54 -13
- package/dist/src/sessions/workspace-discovery.js +99 -37
- package/dist/src/skills/adapters/clawhub.js +10 -8
- package/dist/src/skills/adapters/openclaw.js +10 -7
- package/dist/src/smiths/context-pipeline/parallelism-section.js +192 -3
- package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
- package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
- package/dist/src/smiths/mcpsmith/example.js +1 -1
- package/dist/src/smiths/toolsmith/runtime-discovery.mjs +10 -14
- package/dist/src/storage/backends/logseq.js +68 -7
- package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
- package/dist/src/testing/generators/test-code-generator.js +57 -30
- package/dist/src/uhp/client.js +11 -1
- package/dist/src/uhp/mission.js +5 -0
- package/dist/src/update/checker.mjs +5 -5
- package/dist/src/update/notifier.mjs +54 -2
- package/dist/src/writing/fidelity.js +18 -1
- package/package.json +18 -1
- package/schemas/mission-v1.schema.json +2429 -0
- package/schemas/models/model-catalog.v1.schema.json +301 -0
- package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
- package/schemas/models/model-policy.v1.schema.json +14 -0
- package/schemas/models/provider-model-capabilities.v1.schema.json +1066 -0
- package/tools/_resolve-impl.mjs +39 -0
- package/tools/agents/deploy-agents.mjs +15 -2
- package/tools/agents/providers/base.mjs +63 -43
- package/tools/agents/providers/grokbot.mjs +11 -12
- package/tools/agents/providers/hermes.mjs +6 -3
- package/tools/agents/providers/omp.mjs +5 -2
- package/tools/agents/providers/openclaw-translator.mjs +19 -5
- package/tools/providers/deepseek-harness-transport.mjs +358 -0
- package/tools/providers/omp-transport.mjs +192 -0
- package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
- package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
- package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
- package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
- package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
- package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
- package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
- package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
- package/tools/ralph-external/lib/pi-adapter.mjs +333 -0
- package/tools/ralph-external/lib/provider-adapter.mjs +690 -0
- package/tools/ralph-external/lib/worker-health.mjs +439 -0
- package/vscode-extension/schemas/aiwg.config.v1.json +1953 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode CLI Provider Adapter for External Ralph Loop
|
|
3
|
+
*
|
|
4
|
+
* Provides support for OpenCode as a provider for
|
|
5
|
+
* autonomous task execution in Ralph loops.
|
|
6
|
+
*
|
|
7
|
+
* OpenCode CLI differences from Claude:
|
|
8
|
+
* - Binary: `opencode` instead of `claude`
|
|
9
|
+
* - Headless mode: `opencode run` subcommand
|
|
10
|
+
* - Format flag: `--format json` instead of `--output-format stream-json`
|
|
11
|
+
* - No --dangerously-skip-permissions (no permission bypass needed)
|
|
12
|
+
* - No --print flag (run subcommand is already non-interactive)
|
|
13
|
+
* - No --append-system-prompt (inject into main prompt)
|
|
14
|
+
* - No --max-budget-usd flag
|
|
15
|
+
* - No --max-turns flag
|
|
16
|
+
* - No --mcp-config flag
|
|
17
|
+
* - Session resume via -s/--session instead of --session-id
|
|
18
|
+
* - Model format: provider/model (e.g., opencode/big-pickle, anthropic/claude-sonnet-5)
|
|
19
|
+
* - Agent flag: --agent
|
|
20
|
+
* - JSON output: newline-delimited events, text in type="text" events
|
|
21
|
+
*
|
|
22
|
+
* Model tiers:
|
|
23
|
+
* - Free tier (default): opencode/* models (opencode/big-pickle, etc.)
|
|
24
|
+
* - Anthropic account: anthropic/claude-* models available after `opencode auth add`
|
|
25
|
+
*
|
|
26
|
+
* @implements Plan: Multi-Provider Support for External Ralph Loop
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Version-bound direct-worker surface (roctinam/aiwg#2904, B-OG-39).
|
|
31
|
+
*
|
|
32
|
+
* Qualified against OpenCode CLI 2.0.26 `opencode --help` / `opencode run
|
|
33
|
+
* --help` (help/version syntax captured on host 'noodle' 2026-10-10; see
|
|
34
|
+
* .aiwg/working/s1-scratch/cli-help/opencode-2.0.26.txt) plus one live
|
|
35
|
+
* smoke run and binary disassembly (.aiwg/working/s1-scratch/
|
|
36
|
+
* smoke-opencode.jsonl, 2026-10-10) that redefine final-output validity
|
|
37
|
+
* below; that live evidence is scoped to `parseOutput`'s rule and never
|
|
38
|
+
* promotes the registry's `qualification.status` beyond `unqualified`:
|
|
39
|
+
*
|
|
40
|
+
* opencode run --format json -m M[#E] -f F "Follow the instructions in
|
|
41
|
+
* the attached file exactly."
|
|
42
|
+
*
|
|
43
|
+
* Stdin is confirmed unsupported, not merely undocumented: the conductor
|
|
44
|
+
* disassembled the installed v2.0.26 binary on host 'noodle' and the
|
|
45
|
+
* compiled chunk implementing `run` has no stdin/isTTY handling at all.
|
|
46
|
+
* `run [flags] [<message...>]` takes the message as a trailing positional
|
|
47
|
+
* argument, but `--file`/`-f` ("File to attach to the message") lets the
|
|
48
|
+
* brief ride as an attached file instead: this adapter writes the brief to
|
|
49
|
+
* `briefPath` and passes it via `-f`, so brief bytes never enter argv. The
|
|
50
|
+
* trailing positional is a fixed, content-free instruction
|
|
51
|
+
* (`OPENCODE_FIXED_MESSAGE`) that never carries brief text; `buildWorkerArgs`
|
|
52
|
+
* refuses anything else in that position. `-m/--model` takes
|
|
53
|
+
* `provider/model#variant`; binary disassembly resolves what `#variant`
|
|
54
|
+
* means: the installed CLI builds each model's variant list with a
|
|
55
|
+
* function (minified as `qU(model, manual)`) that maps
|
|
56
|
+
* `model.capabilities.supports.reasoning_effort` (a model-declared string
|
|
57
|
+
* list, e.g. low/medium/high) directly onto variant ids via
|
|
58
|
+
* `{ id: value, settings: { reasoningEffort: value, ... } }`, or — for
|
|
59
|
+
* extended-thinking models exposing `max_thinking_budget` instead — onto
|
|
60
|
+
* two fixed ids, `"max"` and `"high"`. So `#variant` IS a reasoning-effort
|
|
61
|
+
* selector, but the valid id set is per-model (drawn from that model's own
|
|
62
|
+
* catalog capability), not a fixed global enum; `buildWorkerArgs` appends
|
|
63
|
+
* an explicit effort as `#<effort>`. Whether the real CLI refuses an id a
|
|
64
|
+
* model does not expose, or silently falls back to default reasoning, is
|
|
65
|
+
* unverified, so the effort control stays unqualified. There is no `--dir`/`--cwd`
|
|
66
|
+
* flag on `run`: the shared renderer's `cd ${workspace}` sets the process
|
|
67
|
+
* working directory instead. The only permission-adjacent flag is `--auto`
|
|
68
|
+
* ("Auto-approve permissions that are not explicitly denied"), a blanket
|
|
69
|
+
* bypass this profile never emits. What happens to a permission request
|
|
70
|
+
* when `--auto` is absent in non-interactive `run` is not fully resolved:
|
|
71
|
+
* disassembly shows a shared `permission.asked` -> `permission.replied`
|
|
72
|
+
* protocol where an unanswered/unselected request resolves to a `"reject"`
|
|
73
|
+
* outcome (`_==="once"||_==="always"?_:"reject"`, with
|
|
74
|
+
* `requestPermission(...).catch(()=>{return})` on no responder) — this is
|
|
75
|
+
* evidence of reject-on-no-answer rather than an indefinite hang, but it is
|
|
76
|
+
* traced through the ACP/session-protocol layer, not independently
|
|
77
|
+
* reproduced against the plain `run` subcommand, so approval stays
|
|
78
|
+
* `unqualified`/unavailable rather than asserted. `opencode serve` is an
|
|
79
|
+
* HTTP API (not MCP) and `opencode mcp` manages MCP *clients* the agent can
|
|
80
|
+
* call out to, not a worker-serving endpoint, so the AIWG `worker-invoke`
|
|
81
|
+
* bridge remains the MCP route (native `mcp-server`-style hosting is not
|
|
82
|
+
* advertised).
|
|
83
|
+
*
|
|
84
|
+
* Installed-module shape: static `interfaceVersion` and `register` exports
|
|
85
|
+
* so the #2885 installed loader can approve this file by digest under
|
|
86
|
+
* `installed:opencode` (mirroring #2897 codex-adapter.mjs / #2911
|
|
87
|
+
* openhuman-adapter.mjs). The bottom self-registration targets the legacy
|
|
88
|
+
* Ralph-loop registry only; the direct worker profile loads through the
|
|
89
|
+
* installed loader.
|
|
90
|
+
*/
|
|
91
|
+
import { ProviderAdapter, registerProvider } from './provider-adapter.mjs';
|
|
92
|
+
import { classifyWorkerHealthFailure, healthEventMatchesBinding } from './worker-health.mjs';
|
|
93
|
+
|
|
94
|
+
export const OPENCODE_SUPPORTED_VERSIONS = Object.freeze(['2.0.26']);
|
|
95
|
+
/** Stdin is confirmed unsupported by the compiled v2.0.26 `run` chunk (see
|
|
96
|
+
* module doc); the brief travels via `-f`/`--file` instead. */
|
|
97
|
+
export const OPENCODE_DIRECT_STDIN = 'closed';
|
|
98
|
+
/** Fixed, content-free positional message: never the brief, so brief bytes
|
|
99
|
+
* never reach argv even though `-f` carries the real content. */
|
|
100
|
+
export const OPENCODE_FIXED_MESSAGE = 'Follow the instructions in the attached file exactly.';
|
|
101
|
+
/** No scoped sandbox flag is evidenced; `--auto` is a blanket bypass and is
|
|
102
|
+
* never emitted, so the direct profile only accepts externally-enforced
|
|
103
|
+
* sandboxing. */
|
|
104
|
+
export const OPENCODE_DIRECT_SANDBOX_MODES = Object.freeze(['external']);
|
|
105
|
+
/** Generic Ralph-loop aliases resolve through MODEL_MAP below; the direct
|
|
106
|
+
* profile requires an exact observed catalog model instead. */
|
|
107
|
+
const OPENCODE_GENERIC_ALIASES = Object.freeze(['opus', 'sonnet', 'haiku']);
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Model mapping from generic names to OpenCode provider/model format.
|
|
111
|
+
*
|
|
112
|
+
* Free tier uses opencode/* models. Users who have connected an Anthropic
|
|
113
|
+
* account via `opencode auth add` can pass anthropic/* IDs directly
|
|
114
|
+
* (e.g., --model anthropic/claude-sonnet-5) and they pass through unchanged
|
|
115
|
+
* because mapModel() only substitutes known generic aliases.
|
|
116
|
+
*/
|
|
117
|
+
const MODEL_MAP = {
|
|
118
|
+
'opus': 'opencode/big-pickle',
|
|
119
|
+
'sonnet': 'opencode/big-pickle',
|
|
120
|
+
'haiku': 'opencode/big-pickle',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export class OpenCodeAdapter extends ProviderAdapter {
|
|
124
|
+
/**
|
|
125
|
+
* Own JSONL grammar observed from `opencode run --format json`
|
|
126
|
+
* (test/fixtures/providers/opencode/health/stream.ndjson, #2896):
|
|
127
|
+
* `step_start` (a model step began), `text` (streamed answer text in
|
|
128
|
+
* `part.text`) and `step_finish` (the step ended, `part.reason` carries
|
|
129
|
+
* `"stop"` on a clean finish). Handshake stays `process-only`: these are
|
|
130
|
+
* step-scoped events inside one run, not an independently-evidenced
|
|
131
|
+
* session-establishment signal, so only identity correlation (`setup`)
|
|
132
|
+
* and the terminal/error outcome are reported; accepted-start and
|
|
133
|
+
* distinct-item progress remain unavailable pending qualification.
|
|
134
|
+
*
|
|
135
|
+
* `step_start`/`step_finish` are BOTH OPTIONAL (roctinam/aiwg#2904
|
|
136
|
+
* follow-up, live 2.0.26 smoke run + binary disassembly, host 'noodle',
|
|
137
|
+
* 2026-10-10): the run command's event loop returns silently on
|
|
138
|
+
* `session.execution.succeeded` with no JSON event of any kind, and a
|
|
139
|
+
* live run that never produced a `session.step.*` pair emitted a single
|
|
140
|
+
* `text` event and nothing else — confirmed success, not a truncation.
|
|
141
|
+
* Their absence is therefore never treated as failure here; see
|
|
142
|
+
* `parseOutput` for the strict final-validity rule this implies.
|
|
143
|
+
*/
|
|
144
|
+
observeHealthEvent(event, source, binding) {
|
|
145
|
+
if (source.kind !== 'stdout' || !event || typeof event !== 'object') return null;
|
|
146
|
+
if (!healthEventMatchesBinding({ sessionId: event.sessionID }, binding)) return null;
|
|
147
|
+
if (event.type === 'step_start') return { kind: 'setup', sessionId: event.sessionID };
|
|
148
|
+
// --format json is JSONL despite the legacy streamJson=false boolean.
|
|
149
|
+
if (event.type === 'text' && typeof event.part?.text === 'string') {
|
|
150
|
+
return { kind: 'activity', key: event.part.id };
|
|
151
|
+
}
|
|
152
|
+
if (event.type === 'step_finish') {
|
|
153
|
+
if (event.part?.reason === 'stop') return { kind: 'terminal', success: true, key: event.part?.id ?? 'step_finish' };
|
|
154
|
+
return { ...classifyWorkerHealthFailure({ type: 'error', message: event.part?.reason }, { source, provider: 'opencode' }),
|
|
155
|
+
reasonCode: 'PROVIDER_ERROR', fatal: true };
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
/** @returns {string} */
|
|
160
|
+
getBinary() {
|
|
161
|
+
return 'opencode';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** @returns {string} */
|
|
165
|
+
getName() {
|
|
166
|
+
return 'opencode';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* OpenCode has limited capabilities compared to Claude.
|
|
171
|
+
* Supports session resume, model selection, agent mode, and JSON output.
|
|
172
|
+
* @returns {import('./provider-adapter.mjs').ProviderCapabilities}
|
|
173
|
+
*/
|
|
174
|
+
getCapabilities() {
|
|
175
|
+
return {
|
|
176
|
+
streamJson: false,
|
|
177
|
+
sessionResume: true,
|
|
178
|
+
budgetControl: false,
|
|
179
|
+
systemPrompt: false,
|
|
180
|
+
agentMode: true,
|
|
181
|
+
mcpConfig: false,
|
|
182
|
+
maxTurns: false,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Driver-owned argv after the binary for the version-bound direct-shell
|
|
188
|
+
* profile. Every control is explicit: exact observed model via `-m`
|
|
189
|
+
* (with an explicit effort appended as `#<variant>`, see module doc),
|
|
190
|
+
* `--format json` for the evidenced JSONL stream, and the brief as an
|
|
191
|
+
* attached file via `-f` (no stdin transport is evidenced; see the
|
|
192
|
+
* module doc comment) — the positional argument is always the fixed,
|
|
193
|
+
* content-free instruction, never brief text. A sandbox mode other than
|
|
194
|
+
* externally-enforced is refused rather than guessed, since it has no
|
|
195
|
+
* evidenced CLI mapping.
|
|
196
|
+
*
|
|
197
|
+
* @param {{ selected?: { model?: unknown, effort?: { value?: unknown } }, cwd?: unknown, briefPath?: unknown }} invocation
|
|
198
|
+
* @param {unknown} _profile
|
|
199
|
+
* @param {{ sandbox?: unknown }} [permissions]
|
|
200
|
+
* @returns {string[]}
|
|
201
|
+
*/
|
|
202
|
+
buildWorkerArgs(invocation, _profile, permissions) {
|
|
203
|
+
const model = invocation?.selected?.model;
|
|
204
|
+
if (typeof model !== 'string' || !model || model.includes('\0')) {
|
|
205
|
+
throw new Error('OpenCode worker requires an explicit model');
|
|
206
|
+
}
|
|
207
|
+
if (OPENCODE_GENERIC_ALIASES.includes(model.toLowerCase())) {
|
|
208
|
+
throw new Error(`OpenCode worker requires an exact observed model, not the generic alias '${model}'`);
|
|
209
|
+
}
|
|
210
|
+
// #variant is a reasoning-effort selector (binary-evidenced; see module
|
|
211
|
+
// doc), but the valid id set is model-specific and not enumerated here,
|
|
212
|
+
// so a syntactically valid id is appended. Whether the real CLI refuses
|
|
213
|
+
// an undeclared id or silently falls back is unverified (effort stays
|
|
214
|
+
// unqualified).
|
|
215
|
+
const effort = invocation?.selected?.effort?.value ?? null;
|
|
216
|
+
let modelToken = model;
|
|
217
|
+
if (effort !== null) {
|
|
218
|
+
if (typeof effort !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(effort)) {
|
|
219
|
+
throw new Error('OpenCode worker effort must be a plain variant id (no "#", "/", whitespace or control characters)');
|
|
220
|
+
}
|
|
221
|
+
modelToken = `${model}#${effort}`;
|
|
222
|
+
}
|
|
223
|
+
const sandbox = permissions?.sandbox ?? 'external';
|
|
224
|
+
if (!OPENCODE_DIRECT_SANDBOX_MODES.includes(sandbox)) {
|
|
225
|
+
throw new Error(`OpenCode worker sandbox is not a qualified direct mode: ${String(sandbox)}`);
|
|
226
|
+
}
|
|
227
|
+
const cwd = invocation?.cwd;
|
|
228
|
+
if (typeof cwd !== 'string' || !cwd.startsWith('/') || cwd.includes('\0')) {
|
|
229
|
+
throw new Error('OpenCode worker requires an absolute workspace');
|
|
230
|
+
}
|
|
231
|
+
// Stdin is confirmed unsupported (binary disassembly; see module doc),
|
|
232
|
+
// so the brief rides on `-f`/`--file` as a path, never as argv content.
|
|
233
|
+
const briefPath = invocation?.briefPath;
|
|
234
|
+
if (typeof briefPath !== 'string' || !briefPath.startsWith('/') || briefPath.includes('\0')) {
|
|
235
|
+
throw new Error('OpenCode worker requires an absolute brief-file path');
|
|
236
|
+
}
|
|
237
|
+
return ['run', '--format', 'json', '-m', modelToken, '-f', briefPath, OPENCODE_FIXED_MESSAGE];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** No evidenced stdin abort channel for `opencode run`. */
|
|
241
|
+
getAbortInput() {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Build args for the main headless session.
|
|
247
|
+
*
|
|
248
|
+
* OpenCode uses `opencode run --format json` for headless operation.
|
|
249
|
+
*
|
|
250
|
+
* @param {import('./provider-adapter.mjs').SessionArgs} options
|
|
251
|
+
* @returns {string[]}
|
|
252
|
+
*/
|
|
253
|
+
buildSessionArgs(options) {
|
|
254
|
+
const args = [
|
|
255
|
+
'run',
|
|
256
|
+
'--format', 'json',
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
// Model selection (map generic to OpenCode provider/model format)
|
|
260
|
+
if (options.model) {
|
|
261
|
+
args.push('-m', this.mapModel(options.model));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Session resume via -s flag
|
|
265
|
+
if (options.sessionId) {
|
|
266
|
+
args.push('-s', options.sessionId);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Budget control not supported
|
|
270
|
+
if (options.budget) {
|
|
271
|
+
this.warnUnsupported('budgetControl', 'Budget control (--max-budget-usd)');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Max turns not supported
|
|
275
|
+
if (options.maxTurns) {
|
|
276
|
+
this.warnUnsupported('maxTurns', 'Max turns (--max-turns)');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// MCP configuration not supported
|
|
280
|
+
if (options.mcpConfig) {
|
|
281
|
+
this.warnUnsupported('mcpConfig', 'MCP configuration (--mcp-config)');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// System prompt: OpenCode doesn't support --append-system-prompt,
|
|
285
|
+
// so we prepend it to the main prompt
|
|
286
|
+
let prompt = options.prompt;
|
|
287
|
+
if (options.systemPrompt) {
|
|
288
|
+
prompt = `[System Context]\n${options.systemPrompt}\n\n[Task]\n${prompt}`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// The prompt itself (must be last)
|
|
292
|
+
args.push(prompt);
|
|
293
|
+
|
|
294
|
+
return args;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Build args for short analysis calls (spawnSync).
|
|
299
|
+
*
|
|
300
|
+
* @param {import('./provider-adapter.mjs').AnalysisArgs} options
|
|
301
|
+
* @returns {string[]}
|
|
302
|
+
*/
|
|
303
|
+
buildAnalysisArgs(options) {
|
|
304
|
+
const args = [
|
|
305
|
+
'run',
|
|
306
|
+
'--format', 'json',
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
// Model selection
|
|
310
|
+
if (options.model) {
|
|
311
|
+
args.push('-m', this.mapModel(options.model));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Agent flag supported by OpenCode
|
|
315
|
+
if (options.agent) {
|
|
316
|
+
args.push('--agent', options.agent);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// The analysis prompt (must be last)
|
|
320
|
+
args.push(options.prompt);
|
|
321
|
+
|
|
322
|
+
return args;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Map generic model names to OpenCode provider/model format.
|
|
327
|
+
*
|
|
328
|
+
* @param {string} genericModel
|
|
329
|
+
* @returns {string}
|
|
330
|
+
*/
|
|
331
|
+
mapModel(genericModel) {
|
|
332
|
+
const mapped = MODEL_MAP[genericModel.toLowerCase()];
|
|
333
|
+
if (mapped) return mapped;
|
|
334
|
+
// Pass through if already an OpenCode model name or unknown
|
|
335
|
+
return genericModel;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Environment overrides for headless OpenCode sessions.
|
|
340
|
+
* @returns {Object<string, string>}
|
|
341
|
+
*/
|
|
342
|
+
getEnvOverrides() {
|
|
343
|
+
return {
|
|
344
|
+
CI: 'true',
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* OpenCode does not store session transcripts in a known location.
|
|
350
|
+
* @returns {null}
|
|
351
|
+
*/
|
|
352
|
+
getTranscriptPath() {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Parse OpenCode output — extracts text from newline-delimited JSON
|
|
358
|
+
* events under a strict, binary-evidenced final-validity rule
|
|
359
|
+
* (roctinam/aiwg#2904 follow-up, 2026-10-10).
|
|
360
|
+
*
|
|
361
|
+
* There is NO positive session-terminal event in this stream in ANY
|
|
362
|
+
* mode: disassembly of the installed `opencode` 2.0.26 binary shows the
|
|
363
|
+
* `run` command's event loop returns silently on
|
|
364
|
+
* `session.execution.succeeded` ("...if(t.type==="session.execution.
|
|
365
|
+
* succeeded")return}") — no JSON line is ever written for it. A live
|
|
366
|
+
* 2.0.26 smoke run confirmed this empirically: a successful,
|
|
367
|
+
* single-turn reply produced exactly one `text` event and nothing
|
|
368
|
+
* else — no `step_start`, no `step_finish`
|
|
369
|
+
* (.aiwg/working/s1-scratch/smoke-opencode.jsonl). `step_start`/
|
|
370
|
+
* `step_finish` ARE still evidenced for streams that have them (older
|
|
371
|
+
* CLI, or a turn that does produce discrete `session.step.*` events):
|
|
372
|
+
* `step_finish`'s `part.reason` is unconditionally written from
|
|
373
|
+
* `t.data.finish` whenever a step actually ends, so when one IS present
|
|
374
|
+
* it stays authoritative and a non-`"stop"` reason (or an `error`
|
|
375
|
+
* event, emitted from `session.step.failed`/`session.execution.failed`/
|
|
376
|
+
* a non-user `session.execution.interrupted`) still fails the run even
|
|
377
|
+
* at exit 0.
|
|
378
|
+
*
|
|
379
|
+
* The resulting rule, since no positive whole-run terminal exists:
|
|
380
|
+
* reject a malformed/unparseable line, an unrecognized event type, any
|
|
381
|
+
* `error` event, a present `step_finish` whose reason is not `"stop"`,
|
|
382
|
+
* or trailing content after a `step_finish`. Otherwise accept only if
|
|
383
|
+
* at least one `text` part completed (`part.time.end` present and
|
|
384
|
+
* finite) OR a `step_finish` with reason `"stop"` was seen — a `text`
|
|
385
|
+
* event with no recorded `time.end` (e.g. a dropped mid-stream
|
|
386
|
+
* connection) does not count on its own.
|
|
387
|
+
*
|
|
388
|
+
* Residual risk, stated plainly: a provider that truncates the stream
|
|
389
|
+
* right after flushing one complete `text` part, before any further
|
|
390
|
+
* content or an error event, is indistinguishable from a genuine
|
|
391
|
+
* single-part success — there is no stronger signal available from
|
|
392
|
+
* this CLI's `run --format json` output to rule that out.
|
|
393
|
+
*
|
|
394
|
+
* @param {string} stdout
|
|
395
|
+
* @returns {{ text: string }|null}
|
|
396
|
+
*/
|
|
397
|
+
parseOutput(stdout) {
|
|
398
|
+
if (typeof stdout !== 'string' || !stdout.trim()) return null;
|
|
399
|
+
let text = '';
|
|
400
|
+
let sawCompleteText = false;
|
|
401
|
+
let stepFinishStop = null; // null: none seen; otherwise true/false
|
|
402
|
+
let terminalSeen = false;
|
|
403
|
+
for (const raw of stdout.split('\n')) {
|
|
404
|
+
const line = raw.trim();
|
|
405
|
+
if (!line) continue;
|
|
406
|
+
if (terminalSeen) return null; // trailing content after step_finish
|
|
407
|
+
let event;
|
|
408
|
+
try { event = JSON.parse(line); } catch { return null; } // malformed frame: fail closed
|
|
409
|
+
if (!event || typeof event !== 'object' || typeof event.type !== 'string') return null;
|
|
410
|
+
if (event.type === 'step_start') continue;
|
|
411
|
+
else if (event.type === 'text') {
|
|
412
|
+
if (typeof event.part?.text !== 'string') return null;
|
|
413
|
+
text += event.part.text;
|
|
414
|
+
if (Number.isFinite(event.part?.time?.end)) sawCompleteText = true;
|
|
415
|
+
} else if (event.type === 'step_finish') { stepFinishStop = event.part?.reason === 'stop'; terminalSeen = true; }
|
|
416
|
+
else if (event.type === 'error') return null; // binary-evidenced failure event (session.step/execution.failed)
|
|
417
|
+
else return null; // unrecognized event type: never invented, never ignored
|
|
418
|
+
}
|
|
419
|
+
if (stepFinishStop === false) return null;
|
|
420
|
+
if (!sawCompleteText && stepFinishStop !== true) return null; // no positive signal at all
|
|
421
|
+
return { text };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* #2995 round 4 (item 1): strict direct-worker-profile final, separate
|
|
426
|
+
* from the legacy `parseOutput` candidate above (left byte-for-byte
|
|
427
|
+
* unchanged: it is also the legacy Ralph-loop group-mode candidate,
|
|
428
|
+
* `registerProvider('opencode', ...)` below, and `test/unit/
|
|
429
|
+
* ralph-external/worker-finals.test.ts` depends on it falling back to
|
|
430
|
+
* the generic envelope).
|
|
431
|
+
*
|
|
432
|
+
* `step_finish.part.reason` is NOT a two-valued stop/fail field: a
|
|
433
|
+
* multi-step run (tool use, then an answer) ends EACH intermediate step
|
|
434
|
+
* with its own `step_finish`, whose reason names why that step itself
|
|
435
|
+
* ended, not the whole run. The 2.0.26 binary (conductor strings read on
|
|
436
|
+
* noodle, 2026-10-11) normalizes finish reasons to `stop`, `length`,
|
|
437
|
+
* `tool-calls`, `content-filter`, `error`, `other` and `unknown`, and its
|
|
438
|
+
* own UI code treats exactly `tool-calls` (and `unknown`) as not final:
|
|
439
|
+
* `T.finish && !["tool-calls","unknown"].includes(T.finish)`. So
|
|
440
|
+
* `tool-calls` is the one evidenced "this step ended because the model
|
|
441
|
+
* called a tool; another step follows" reason. Every other reason,
|
|
442
|
+
* including `unknown` and any unrecognized value, fails closed:
|
|
443
|
+
* CONTINUE_REASONS is the only carve-out from "non-stop fails".
|
|
444
|
+
*
|
|
445
|
+
* Algorithm: track one candidate per OPEN step (`step_start` always
|
|
446
|
+
* resets it — an earlier step's text is never the final answer).
|
|
447
|
+
* Within a step, a later COMPLETE (`time.end` present) `text` part
|
|
448
|
+
* REPLACES the candidate (no evidence of a within-step text boundary
|
|
449
|
+
* requiring concatenation); an INCOMPLETE `text` part arriving after a
|
|
450
|
+
* complete candidate already exists corrupts that step's candidate
|
|
451
|
+
* permanently (a dropped/truncated stream is never trusted over an
|
|
452
|
+
* earlier value). A `step_finish` with reason `"stop"` is terminal
|
|
453
|
+
* ONLY when nothing legitimately follows it (enforced structurally,
|
|
454
|
+
* same as before) and requires a non-corrupted, non-empty candidate
|
|
455
|
+
* from THAT step. A `step_finish` with a CONTINUE_REASONS reason is not
|
|
456
|
+
* terminal and not a failure: the run proceeds (the next `step_start`
|
|
457
|
+
* resets the candidate as normal). Any other `step_finish` reason, or
|
|
458
|
+
* an `error` event, fails closed. When the stream has no step markers
|
|
459
|
+
* at all (the live 2.0.26 shape evidenced in .aiwg/working/s1-scratch/
|
|
460
|
+
* smoke-opencode.jsonl — a single `text` event and nothing else), the
|
|
461
|
+
* same replace/corrupt rule applies to the whole stream's standalone
|
|
462
|
+
* `text` events.
|
|
463
|
+
* @param {string} stdout
|
|
464
|
+
* @returns {{ text: string }|null}
|
|
465
|
+
*/
|
|
466
|
+
parseDirectFinal(stdout) {
|
|
467
|
+
if (typeof stdout !== 'string' || !stdout.trim()) return null;
|
|
468
|
+
const CONTINUE_REASONS = ['tool-calls'];
|
|
469
|
+
let sawAnyStep = false;
|
|
470
|
+
let stepText = null;
|
|
471
|
+
let stepCorrupted = false;
|
|
472
|
+
let finalStepText = null;
|
|
473
|
+
let terminal = false;
|
|
474
|
+
let failed = false;
|
|
475
|
+
for (const raw of stdout.split('\n')) {
|
|
476
|
+
const line = raw.trim();
|
|
477
|
+
if (!line) continue;
|
|
478
|
+
if (terminal) return null; // nothing may follow a terminal step_finish(stop)
|
|
479
|
+
let event;
|
|
480
|
+
try { event = JSON.parse(line); } catch { return null; }
|
|
481
|
+
if (!event || typeof event !== 'object' || typeof event.type !== 'string') return null;
|
|
482
|
+
if (event.type === 'step_start') {
|
|
483
|
+
// A new step begins: any earlier step's candidate is never the final answer.
|
|
484
|
+
sawAnyStep = true; stepText = null; stepCorrupted = false;
|
|
485
|
+
} else if (event.type === 'text') {
|
|
486
|
+
if (typeof event.part?.text !== 'string') return null;
|
|
487
|
+
if (Number.isFinite(event.part?.time?.end)) {
|
|
488
|
+
stepText = event.part.text; // a later complete part REPLACES, never concatenates
|
|
489
|
+
} else {
|
|
490
|
+
// An incomplete part after an established candidate corrupts it
|
|
491
|
+
// permanently; an incomplete part with no prior candidate is
|
|
492
|
+
// simply "no positive signal yet" (handled by the end-of-stream
|
|
493
|
+
// checks below).
|
|
494
|
+
if (stepText !== null) stepCorrupted = true;
|
|
495
|
+
}
|
|
496
|
+
} else if (event.type === 'step_finish') {
|
|
497
|
+
sawAnyStep = true;
|
|
498
|
+
const reason = event.part?.reason;
|
|
499
|
+
if (reason === 'stop') {
|
|
500
|
+
terminal = true;
|
|
501
|
+
finalStepText = stepCorrupted ? null : stepText;
|
|
502
|
+
} else if (!CONTINUE_REASONS.includes(reason)) {
|
|
503
|
+
failed = true; // a recognized-failure or unrecognized reason fails closed
|
|
504
|
+
}
|
|
505
|
+
// CONTINUE_REASONS: not terminal, not a failure; the next step_start resets the candidate.
|
|
506
|
+
} else if (event.type === 'error') return null;
|
|
507
|
+
// The 2.0.26 run emitter writes exactly six event types: error, reasoning, step_finish, step_start, text and
|
|
508
|
+
// tool_use (conductor strings read on noodle, 2026-10-11). tool_use and reasoning are legitimate non-answer
|
|
509
|
+
// events: they never contribute answer text. Anything else is outside the emitter's vocabulary and rejects.
|
|
510
|
+
else if (event.type !== 'tool_use' && event.type !== 'reasoning') return null;
|
|
511
|
+
}
|
|
512
|
+
if (failed) return null;
|
|
513
|
+
if (sawAnyStep) {
|
|
514
|
+
if (!terminal || finalStepText === null || !finalStepText.trim()) return null;
|
|
515
|
+
return { text: finalStepText };
|
|
516
|
+
}
|
|
517
|
+
if (stepCorrupted || stepText === null || !stepText.trim()) return null;
|
|
518
|
+
return { text: stepText };
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Self-register on import for the legacy Ralph loop only. The direct worker
|
|
523
|
+
// profile loads through the #2885 installed loader instead (see below).
|
|
524
|
+
registerProvider('opencode', () => new OpenCodeAdapter());
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* #2995 round 3: `OpenCodeDirectAdapter` is the Claude-precedent `-direct`
|
|
528
|
+
* subclass, registered under its OWN id (`opencode-direct`, never the
|
|
529
|
+
* bare `opencode` legacy id above) so loading this installed module
|
|
530
|
+
* cannot overwrite the legacy registration in-process (src/models/
|
|
531
|
+
* worker-installation.mjs ~l.550). `parseOutput` here is the strict,
|
|
532
|
+
* last-step-only `parseDirectFinal` rule (never the loose, concatenating,
|
|
533
|
+
* generic-accepting legacy candidate), and `parseWorkerFinal` is strict
|
|
534
|
+
* with no generic-envelope fallback: this class is never reachable from
|
|
535
|
+
* the legacy bare id, so the legacy group-mode contract (test/unit/
|
|
536
|
+
* ralph-external/worker-finals.test.ts) never sees it.
|
|
537
|
+
*/
|
|
538
|
+
export class OpenCodeDirectAdapter extends OpenCodeAdapter {
|
|
539
|
+
// #2995 round 4 (item 3): production resolves the installed adapter by
|
|
540
|
+
// `adapter.id` (now `opencode-direct`, item B) and src/models/
|
|
541
|
+
// worker-installation.mjs's `inspectWorkerInstallation` requires
|
|
542
|
+
// `adapter.id === instance.getName()`. `getBinary()` and the
|
|
543
|
+
// `provider: 'opencode'` literal inside `observeHealthEvent` are
|
|
544
|
+
// untouched.
|
|
545
|
+
getName() { return 'opencode-direct'; }
|
|
546
|
+
|
|
547
|
+
parseOutput(stdout) {
|
|
548
|
+
return this.parseDirectFinal(stdout);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* @param {string|Uint8Array} output
|
|
553
|
+
* @param {Object} [options]
|
|
554
|
+
* @returns {Promise<import('../../../src/mission-protocol/types.js').WorkerClaims>}
|
|
555
|
+
*/
|
|
556
|
+
async parseWorkerFinal(output, options = {}) {
|
|
557
|
+
if (options.format === 'mcp') return super.parseWorkerFinal(output, options);
|
|
558
|
+
const text = typeof output === 'string' ? output : Buffer.from(output).toString('utf8');
|
|
559
|
+
const final = this.parseDirectFinal(text);
|
|
560
|
+
if (!final || typeof final.text !== 'string' || !final.text.trim()) {
|
|
561
|
+
throw new Error('OpenCode worker stream did not produce a successful final answer');
|
|
562
|
+
}
|
|
563
|
+
return super.parseWorkerFinal(final.text, { ...options, format: 'json' });
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** Installed-module interface version (mirrors #2897 codex-adapter.mjs). */
|
|
568
|
+
export const interfaceVersion = 1;
|
|
569
|
+
|
|
570
|
+
/** Installed-module registration: exactly the one approved adapter ID. */
|
|
571
|
+
export function register(factory) {
|
|
572
|
+
factory('opencode-direct', () => new OpenCodeDirectAdapter());
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export default OpenCodeAdapter;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenHuman direct-worker driver (B-OG-47, roctinam/aiwg#2911).
|
|
3
|
+
*
|
|
4
|
+
* Version-bound to the source-evidenced `openhuman-core` 0.64.14 one-shot RPC
|
|
5
|
+
* call (tinyhumansai/openhuman@216ea322a3c1269dada70751ac5c08eac7dc7596):
|
|
6
|
+
*
|
|
7
|
+
* openhuman-core --model M call --method openhuman.agent_chat --params-stdin
|
|
8
|
+
*
|
|
9
|
+
* `--params-stdin` reads stdin to EOF and is mutually exclusive with
|
|
10
|
+
* `--params` (crates/openhuman-core/src/core/cli.rs:445-468); the result is
|
|
11
|
+
* pretty-printed JSON on stdout (cli.rs:486). The params document
|
|
12
|
+
* (`message`, `model_override`, `cwd`; agent/schemas.rs:97-105) is framed by
|
|
13
|
+
* the conductor from the brief (`stdin: 'brief-json'`,
|
|
14
|
+
* src/serve/worker-stdin-frame.mjs), so prompt text never enters argv.
|
|
15
|
+
*
|
|
16
|
+
* Installed-module shape: self-contained, with static `interfaceVersion` and
|
|
17
|
+
* `register` exports, so the #2885 loader can approve it by digest under
|
|
18
|
+
* `installed:openhuman`. It is not a Ralph loop provider (no session,
|
|
19
|
+
* resume, abort or effort surface is evidenced). Documentation/source
|
|
20
|
+
* evidence and synthetic fixtures never qualify it: the bundled profile stays
|
|
21
|
+
* unqualified until an authorized live run.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export const interfaceVersion = 1;
|
|
25
|
+
|
|
26
|
+
export const OPENHUMAN_SUPPORTED_VERSIONS = Object.freeze(['0.64.14']);
|
|
27
|
+
export const OPENHUMAN_SOURCE_REVISION = '216ea322a3c1269dada70751ac5c08eac7dc7596';
|
|
28
|
+
export const OPENHUMAN_CHAT_METHOD = 'openhuman.agent_chat';
|
|
29
|
+
|
|
30
|
+
export class OpenHumanAdapter {
|
|
31
|
+
getBinary() { return 'openhuman-core'; }
|
|
32
|
+
getName() { return 'openhuman'; }
|
|
33
|
+
getCapabilities() {
|
|
34
|
+
return { streamJson: false, sessionResume: false, budgetControl: false, systemPrompt: false,
|
|
35
|
+
agentMode: false, mcpConfig: false, maxTurns: false, rpcAbort: false };
|
|
36
|
+
}
|
|
37
|
+
buildSessionArgs() {
|
|
38
|
+
throw new Error('OpenHuman has no qualified Ralph loop session; use the direct worker profile');
|
|
39
|
+
}
|
|
40
|
+
buildAnalysisArgs() { return this.buildSessionArgs(); }
|
|
41
|
+
|
|
42
|
+
/** Driver-owned argv after the binary: no brief text, no permission flags
|
|
43
|
+
* (no per-call sandbox/approval control is evidenced; the qualified external
|
|
44
|
+
* boundary owns them) and never a guessed effort. */
|
|
45
|
+
buildWorkerArgs(invocation, _profile, _permissions) {
|
|
46
|
+
const effort = invocation?.selected?.effort?.value ?? null;
|
|
47
|
+
if (effort !== null) throw new Error('OpenHuman has no evidenced effort control');
|
|
48
|
+
const model = invocation?.selected?.model;
|
|
49
|
+
if (typeof model !== 'string' || !model || model.includes('\0')) throw new Error('OpenHuman worker requires an explicit model');
|
|
50
|
+
const cwd = invocation?.cwd;
|
|
51
|
+
if (typeof cwd !== 'string' || !cwd.startsWith('/') || cwd.includes('\0')) throw new Error('OpenHuman worker requires an absolute workspace');
|
|
52
|
+
return ['--model', model, 'call', '--method', OPENHUMAN_CHAT_METHOD, '--params-stdin'];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Final-only output: no stream grammar is evidenced, so no event is invented. */
|
|
56
|
+
observeHealthEvent() { return null; }
|
|
57
|
+
|
|
58
|
+
/** Strict single JSON object; truncated or non-object output is not a final. */
|
|
59
|
+
parseOutput(stdout) {
|
|
60
|
+
if (typeof stdout !== 'string' || !stdout.trim()) return null;
|
|
61
|
+
try {
|
|
62
|
+
const value = JSON.parse(stdout);
|
|
63
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
64
|
+
} catch { return null; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getAbortInput() { return null; }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Installed-module registration: exactly the one approved adapter ID. */
|
|
71
|
+
export function register(factory) {
|
|
72
|
+
factory('openhuman', () => new OpenHumanAdapter());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default OpenHumanAdapter;
|