@amends-dev/harness-opencode 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/Opencode2Agent.d.ts +105 -0
- package/Opencode2Agent.d.ts.map +1 -0
- package/Opencode2Agent.js +72 -0
- package/Opencode2Agent.js.map +1 -0
- package/OpencodeAgent.d.ts +110 -0
- package/OpencodeAgent.d.ts.map +1 -0
- package/OpencodeAgent.js +83 -0
- package/OpencodeAgent.js.map +1 -0
- package/README.md +221 -2
- package/index.d.ts +18 -0
- package/index.d.ts.map +1 -0
- package/index.js +19 -0
- package/index.js.map +1 -0
- package/internal/agent.d.ts +29 -0
- package/internal/agent.d.ts.map +1 -0
- package/internal/agent.js +34 -0
- package/internal/agent.js.map +1 -0
- package/internal/bridge/bunBridge.d.ts +33 -0
- package/internal/bridge/bunBridge.d.ts.map +1 -0
- package/internal/bridge/bunBridge.js +62 -0
- package/internal/bridge/bunBridge.js.map +1 -0
- package/internal/bridge/driver.d.ts +81 -0
- package/internal/bridge/driver.d.ts.map +1 -0
- package/internal/bridge/driver.js +82 -0
- package/internal/bridge/driver.js.map +1 -0
- package/internal/bridge/hostTools.d.ts +28 -0
- package/internal/bridge/hostTools.d.ts.map +1 -0
- package/internal/bridge/hostTools.js +41 -0
- package/internal/bridge/hostTools.js.map +1 -0
- package/internal/bridge/server.d.ts +64 -0
- package/internal/bridge/server.d.ts.map +1 -0
- package/internal/bridge/server.js +129 -0
- package/internal/bridge/server.js.map +1 -0
- package/internal/bridge/steps.d.ts +29 -0
- package/internal/bridge/steps.d.ts.map +1 -0
- package/internal/bridge/steps.js +42 -0
- package/internal/bridge/steps.js.map +1 -0
- package/internal/bridge/v1/api.d.ts +181 -0
- package/internal/bridge/v1/api.d.ts.map +1 -0
- package/internal/bridge/v1/api.js +83 -0
- package/internal/bridge/v1/api.js.map +1 -0
- package/internal/bridge/v1/event.d.ts +597 -0
- package/internal/bridge/v1/event.d.ts.map +1 -0
- package/internal/bridge/v1/event.js +186 -0
- package/internal/bridge/v1/event.js.map +1 -0
- package/internal/bridge/v1/main.d.ts +6 -0
- package/internal/bridge/v1/main.d.ts.map +1 -0
- package/internal/bridge/v1/main.js +8 -0
- package/internal/bridge/v1/main.js.map +1 -0
- package/internal/bridge/v1/prompt.d.ts +18 -0
- package/internal/bridge/v1/prompt.d.ts.map +1 -0
- package/internal/bridge/v1/prompt.js +92 -0
- package/internal/bridge/v1/prompt.js.map +1 -0
- package/internal/bridge/v1/session.d.ts +28 -0
- package/internal/bridge/v1/session.d.ts.map +1 -0
- package/internal/bridge/v1/session.js +188 -0
- package/internal/bridge/v1/session.js.map +1 -0
- package/internal/bridge/v1/turn.d.ts +82 -0
- package/internal/bridge/v1/turn.d.ts.map +1 -0
- package/internal/bridge/v1/turn.js +304 -0
- package/internal/bridge/v1/turn.js.map +1 -0
- package/internal/bridge/v2/api.d.ts +86 -0
- package/internal/bridge/v2/api.d.ts.map +1 -0
- package/internal/bridge/v2/api.js +105 -0
- package/internal/bridge/v2/api.js.map +1 -0
- package/internal/bridge/v2/event.d.ts +539 -0
- package/internal/bridge/v2/event.d.ts.map +1 -0
- package/internal/bridge/v2/event.js +164 -0
- package/internal/bridge/v2/event.js.map +1 -0
- package/internal/bridge/v2/main.d.ts +6 -0
- package/internal/bridge/v2/main.d.ts.map +1 -0
- package/internal/bridge/v2/main.js +8 -0
- package/internal/bridge/v2/main.js.map +1 -0
- package/internal/bridge/v2/prompt.d.ts +109 -0
- package/internal/bridge/v2/prompt.d.ts.map +1 -0
- package/internal/bridge/v2/prompt.js +209 -0
- package/internal/bridge/v2/prompt.js.map +1 -0
- package/internal/bridge/v2/session.d.ts +21 -0
- package/internal/bridge/v2/session.d.ts.map +1 -0
- package/internal/bridge/v2/session.js +198 -0
- package/internal/bridge/v2/session.js.map +1 -0
- package/internal/bridge/v2/turn.d.ts +62 -0
- package/internal/bridge/v2/turn.d.ts.map +1 -0
- package/internal/bridge/v2/turn.js +277 -0
- package/internal/bridge/v2/turn.js.map +1 -0
- package/internal/models.d.ts +47 -0
- package/internal/models.d.ts.map +1 -0
- package/internal/models.js +170 -0
- package/internal/models.js.map +1 -0
- package/internal/template.d.ts +46 -0
- package/internal/template.d.ts.map +1 -0
- package/internal/template.js +267 -0
- package/internal/template.js.map +1 -0
- package/package.json +39 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sarib Mahmood
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opencode as an agent that hosts run: opencode 2, driven over its session API, which takes a
|
|
3
|
+
* turn's prompt with its files, its instructions, given beside opencode's own, the host's tools,
|
|
4
|
+
* and requests to compact, and records a session started on another agent as opencode's own. It
|
|
5
|
+
* does not answer in JSON yet, which fails with `UnsupportedError`. opencode works in the sandbox
|
|
6
|
+
* behind its bridge: the agent's template installs both there, and a session launches the bridge
|
|
7
|
+
* when none answers. One bridge and one opencode server serve every session that hosts start in a
|
|
8
|
+
* sandbox with the same options, whichever host started it; other options, such as another model,
|
|
9
|
+
* get a bridge and a server of their own. What runs in the sandbox is in `internal/bridge/v2/`.
|
|
10
|
+
*
|
|
11
|
+
* opencode runs on the model endpoints the agent is made with, which it is configured for.
|
|
12
|
+
* Their keys reach opencode in its configuration, in the sandbox, where the commands opencode runs
|
|
13
|
+
* can read them. The host's tools reach opencode as an MCP server the bridge serves on the
|
|
14
|
+
* sandbox's loopback, which opencode 2's model calls from code it runs; the host runs them, and
|
|
15
|
+
* nothing they need reaches the sandbox.
|
|
16
|
+
*
|
|
17
|
+
* @since 0.1.0
|
|
18
|
+
*/
|
|
19
|
+
import type { Agent } from "@amends-dev/harness";
|
|
20
|
+
import type { Sandbox } from "@amends-dev/sandbox";
|
|
21
|
+
import type * as Redacted from "effect/Redacted";
|
|
22
|
+
import type * as Schema from "effect/Schema";
|
|
23
|
+
/**
|
|
24
|
+
* What the agent is made with: the endpoints its turns run on, as `Agent.Options` give them, and
|
|
25
|
+
* what only opencode takes.
|
|
26
|
+
*
|
|
27
|
+
* **Details**
|
|
28
|
+
*
|
|
29
|
+
* All of it, the keys included, names the bridge, so an agent made with other options gets a bridge
|
|
30
|
+
* and a server of its own in a sandbox, and never runs on another's.
|
|
31
|
+
*
|
|
32
|
+
* @category options
|
|
33
|
+
* @since 0.1.0
|
|
34
|
+
*/
|
|
35
|
+
export interface Options extends Agent.Options {
|
|
36
|
+
/**
|
|
37
|
+
* The token the bridge in the sandbox `id` takes, derived from the id with a secret every host
|
|
38
|
+
* shares, such as an HMAC of it, so that every host connects to a bridge any of them launched.
|
|
39
|
+
*/
|
|
40
|
+
readonly token: (id: Sandbox.SandboxId) => Redacted.Redacted<string>;
|
|
41
|
+
/**
|
|
42
|
+
* opencode's configuration, as opencode 2 writes its `opencode.json`, which names providers
|
|
43
|
+
* `providers`; the providers that serve the models replace any it has. It takes effect when the
|
|
44
|
+
* bridge starts opencode's server; a server already running in the sandbox keeps the one it
|
|
45
|
+
* started with.
|
|
46
|
+
*/
|
|
47
|
+
readonly config?: {
|
|
48
|
+
readonly [key: string]: Schema.Json;
|
|
49
|
+
};
|
|
50
|
+
/** Environment variables opencode's server runs with. */
|
|
51
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The agent, which bundles the bridge program for its template when it is made, with `Bun.build`,
|
|
55
|
+
* so the host runs on Bun.
|
|
56
|
+
*
|
|
57
|
+
* **Details**
|
|
58
|
+
*
|
|
59
|
+
* Each session reaches opencode through the bridge in its sandbox, with the `Crypto` and the
|
|
60
|
+
* `WebSocketConstructor` the agent is made with; the constructor must send headers, as Bun's and
|
|
61
|
+
* Node's do. Endpoints that opencode cannot serve together fail with `UnsupportedError`.
|
|
62
|
+
*
|
|
63
|
+
* **Example** (A turn of opencode 2 in a new sandbox)
|
|
64
|
+
*
|
|
65
|
+
* ```ts
|
|
66
|
+
* import { createHmac } from "node:crypto"
|
|
67
|
+
* import { Agent, Harness } from "@amends-dev/harness"
|
|
68
|
+
* import { Opencode2Agent } from "@amends-dev/harness-opencode"
|
|
69
|
+
* import { SandboxProvider } from "@amends-dev/sandbox"
|
|
70
|
+
* import { BunServices, BunSocket } from "@effect/platform-bun"
|
|
71
|
+
* import { Config, Effect, Layer, Redacted } from "effect"
|
|
72
|
+
*
|
|
73
|
+
* declare const sandboxes: Layer.Layer<SandboxProvider.SandboxProvider> // a sandbox provider
|
|
74
|
+
*
|
|
75
|
+
* const program = Effect.gen(function* () {
|
|
76
|
+
* // Every host derives a sandbox's bridge token from its id with a secret the hosts share.
|
|
77
|
+
* const secret = yield* Config.Redacted("BRIDGE_SECRET")
|
|
78
|
+
* const agent = yield* Opencode2Agent.make({
|
|
79
|
+
* model: {
|
|
80
|
+
* provider: "anthropic",
|
|
81
|
+
* model: "claude-sonnet-5-5",
|
|
82
|
+
* protocol: "anthropic-messages",
|
|
83
|
+
* url: "https://api.anthropic.com/v1",
|
|
84
|
+
* apiKey: yield* Config.Redacted("ANTHROPIC_API_KEY")
|
|
85
|
+
* },
|
|
86
|
+
* token: (sandboxId) =>
|
|
87
|
+
* Redacted.make(createHmac("sha256", Redacted.value(secret)).update(sandboxId).digest("base64url"))
|
|
88
|
+
* })
|
|
89
|
+
* const harness = Harness.make(agent)
|
|
90
|
+
* const sandboxProvider = yield* SandboxProvider.SandboxProvider
|
|
91
|
+
* const sandbox = yield* sandboxProvider.createScoped({ template: harness.agent.template })
|
|
92
|
+
* const session = yield* harness.createSession({ sandbox, id: Agent.SessionId.make("review") })
|
|
93
|
+
* const answer = yield* session.generate({ prompt: "Summarize README.md" })
|
|
94
|
+
* return answer.text
|
|
95
|
+
* }).pipe(
|
|
96
|
+
* Effect.scoped,
|
|
97
|
+
* Effect.provide(Layer.mergeAll(sandboxes, BunServices.layer, BunSocket.layerWebSocketConstructor))
|
|
98
|
+
* )
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* @category constructors
|
|
102
|
+
* @since 0.1.0
|
|
103
|
+
*/
|
|
104
|
+
export declare const make: (options: Options) => import("effect/Effect").Effect<Agent.Agent, import("@amends-dev/harness/HarnessError").HarnessError | import("effect/PlatformError").PlatformError, import("effect/Crypto").Crypto | import("effect/socket/Socket").WebSocketConstructor>;
|
|
105
|
+
//# sourceMappingURL=Opencode2Agent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Opencode2Agent.d.ts","sourceRoot":"","sources":["../src/Opencode2Agent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,KAAK,QAAQ,MAAM,iBAAiB,CAAA;AAChD,OAAO,KAAK,KAAK,MAAM,MAAM,eAAe,CAAA;AAG5C;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,OAAQ,SAAQ,KAAK,CAAC,OAAO;IAC5C;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,OAAO,CAAC,SAAS,KAAK,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAA;IAEpE;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,IAAI,CAAA;KAAE,CAAA;IAEzD,yDAAyD;IACzD,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CAChD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,eAAO,MAAM,IAAI,YAAa,OAAO,8OAA4D,CAAA"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opencode as an agent that hosts run: opencode 2, driven over its session API, which takes a
|
|
3
|
+
* turn's prompt with its files, its instructions, given beside opencode's own, the host's tools,
|
|
4
|
+
* and requests to compact, and records a session started on another agent as opencode's own. It
|
|
5
|
+
* does not answer in JSON yet, which fails with `UnsupportedError`. opencode works in the sandbox
|
|
6
|
+
* behind its bridge: the agent's template installs both there, and a session launches the bridge
|
|
7
|
+
* when none answers. One bridge and one opencode server serve every session that hosts start in a
|
|
8
|
+
* sandbox with the same options, whichever host started it; other options, such as another model,
|
|
9
|
+
* get a bridge and a server of their own. What runs in the sandbox is in `internal/bridge/v2/`.
|
|
10
|
+
*
|
|
11
|
+
* opencode runs on the model endpoints the agent is made with, which it is configured for.
|
|
12
|
+
* Their keys reach opencode in its configuration, in the sandbox, where the commands opencode runs
|
|
13
|
+
* can read them. The host's tools reach opencode as an MCP server the bridge serves on the
|
|
14
|
+
* sandbox's loopback, which opencode 2's model calls from code it runs; the host runs them, and
|
|
15
|
+
* nothing they need reaches the sandbox.
|
|
16
|
+
*
|
|
17
|
+
* @since 0.1.0
|
|
18
|
+
*/
|
|
19
|
+
import * as internal from "./internal/agent.js";
|
|
20
|
+
/**
|
|
21
|
+
* The agent, which bundles the bridge program for its template when it is made, with `Bun.build`,
|
|
22
|
+
* so the host runs on Bun.
|
|
23
|
+
*
|
|
24
|
+
* **Details**
|
|
25
|
+
*
|
|
26
|
+
* Each session reaches opencode through the bridge in its sandbox, with the `Crypto` and the
|
|
27
|
+
* `WebSocketConstructor` the agent is made with; the constructor must send headers, as Bun's and
|
|
28
|
+
* Node's do. Endpoints that opencode cannot serve together fail with `UnsupportedError`.
|
|
29
|
+
*
|
|
30
|
+
* **Example** (A turn of opencode 2 in a new sandbox)
|
|
31
|
+
*
|
|
32
|
+
* ```ts
|
|
33
|
+
* import { createHmac } from "node:crypto"
|
|
34
|
+
* import { Agent, Harness } from "@amends-dev/harness"
|
|
35
|
+
* import { Opencode2Agent } from "@amends-dev/harness-opencode"
|
|
36
|
+
* import { SandboxProvider } from "@amends-dev/sandbox"
|
|
37
|
+
* import { BunServices, BunSocket } from "@effect/platform-bun"
|
|
38
|
+
* import { Config, Effect, Layer, Redacted } from "effect"
|
|
39
|
+
*
|
|
40
|
+
* declare const sandboxes: Layer.Layer<SandboxProvider.SandboxProvider> // a sandbox provider
|
|
41
|
+
*
|
|
42
|
+
* const program = Effect.gen(function* () {
|
|
43
|
+
* // Every host derives a sandbox's bridge token from its id with a secret the hosts share.
|
|
44
|
+
* const secret = yield* Config.Redacted("BRIDGE_SECRET")
|
|
45
|
+
* const agent = yield* Opencode2Agent.make({
|
|
46
|
+
* model: {
|
|
47
|
+
* provider: "anthropic",
|
|
48
|
+
* model: "claude-sonnet-5-5",
|
|
49
|
+
* protocol: "anthropic-messages",
|
|
50
|
+
* url: "https://api.anthropic.com/v1",
|
|
51
|
+
* apiKey: yield* Config.Redacted("ANTHROPIC_API_KEY")
|
|
52
|
+
* },
|
|
53
|
+
* token: (sandboxId) =>
|
|
54
|
+
* Redacted.make(createHmac("sha256", Redacted.value(secret)).update(sandboxId).digest("base64url"))
|
|
55
|
+
* })
|
|
56
|
+
* const harness = Harness.make(agent)
|
|
57
|
+
* const sandboxProvider = yield* SandboxProvider.SandboxProvider
|
|
58
|
+
* const sandbox = yield* sandboxProvider.createScoped({ template: harness.agent.template })
|
|
59
|
+
* const session = yield* harness.createSession({ sandbox, id: Agent.SessionId.make("review") })
|
|
60
|
+
* const answer = yield* session.generate({ prompt: "Summarize README.md" })
|
|
61
|
+
* return answer.text
|
|
62
|
+
* }).pipe(
|
|
63
|
+
* Effect.scoped,
|
|
64
|
+
* Effect.provide(Layer.mergeAll(sandboxes, BunServices.layer, BunSocket.layerWebSocketConstructor))
|
|
65
|
+
* )
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* @category constructors
|
|
69
|
+
* @since 0.1.0
|
|
70
|
+
*/
|
|
71
|
+
export const make = (options) => internal.make({ id: "opencode2", version: 2 }, options);
|
|
72
|
+
//# sourceMappingURL=Opencode2Agent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Opencode2Agent.js","sourceRoot":"","sources":["../src/Opencode2Agent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAMH,OAAO,KAAK,QAAQ,MAAM,qBAAqB,CAAA;AAiC/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,OAAgB,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,OAAO,CAAC,CAAA"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opencode as an agent that hosts run: opencode 1, driven over its session API, which takes all of
|
|
3
|
+
* a turn: its instructions, as the system prompt of its prompt, an answer in JSON, the files of its
|
|
4
|
+
* prompt, the host's tools, and requests to compact. opencode works in the sandbox behind its
|
|
5
|
+
* bridge: the agent's template installs both there, and a session launches the bridge when none
|
|
6
|
+
* answers. One bridge and one opencode server serve every session that hosts start in a sandbox
|
|
7
|
+
* with the same options, whichever host started it; other options, such as another model, get a
|
|
8
|
+
* bridge and a server of their own. What runs in the sandbox is in `internal/bridge/v1/`.
|
|
9
|
+
*
|
|
10
|
+
* opencode runs on the model endpoints the agent is made with, which it is configured for.
|
|
11
|
+
* Their keys reach opencode in its configuration, in the sandbox, where the commands opencode runs
|
|
12
|
+
* can read them. The host's tools reach opencode as an MCP server the bridge serves on the
|
|
13
|
+
* sandbox's loopback, whose tools opencode names `host_<tool>`; the host runs them, and nothing
|
|
14
|
+
* they need reaches the sandbox.
|
|
15
|
+
*
|
|
16
|
+
* A session started on another agent is written into opencode as one message the model reads,
|
|
17
|
+
* since opencode 1 records no steps but its own. `Opencode2Agent`, over opencode 2's session API,
|
|
18
|
+
* records them as opencode's own, but does not answer in JSON yet.
|
|
19
|
+
*
|
|
20
|
+
* @since 0.1.0
|
|
21
|
+
*/
|
|
22
|
+
import type { Agent } from "@amends-dev/harness";
|
|
23
|
+
import type { Sandbox } from "@amends-dev/sandbox";
|
|
24
|
+
import type * as Redacted from "effect/Redacted";
|
|
25
|
+
import type * as Schema from "effect/Schema";
|
|
26
|
+
/**
|
|
27
|
+
* What the agent is made with: the endpoints its turns run on, as `Agent.Options` give them, and
|
|
28
|
+
* what only opencode takes.
|
|
29
|
+
*
|
|
30
|
+
* **Details**
|
|
31
|
+
*
|
|
32
|
+
* All of it, the keys included, names the bridge, so an agent made with other options gets a bridge
|
|
33
|
+
* and a server of its own in a sandbox, and never runs on another's.
|
|
34
|
+
*
|
|
35
|
+
* @category options
|
|
36
|
+
* @since 0.1.0
|
|
37
|
+
*/
|
|
38
|
+
export interface Options extends Agent.Options {
|
|
39
|
+
/**
|
|
40
|
+
* The token the bridge in the sandbox `id` takes, derived from the id with a secret every host
|
|
41
|
+
* shares, such as an HMAC of it, so that every host connects to a bridge any of them launched.
|
|
42
|
+
*/
|
|
43
|
+
readonly token: (id: Sandbox.SandboxId) => Redacted.Redacted<string>;
|
|
44
|
+
/**
|
|
45
|
+
* opencode's configuration, as opencode 1 writes its `opencode.json`, which names providers
|
|
46
|
+
* `provider`; the providers that serve the models replace any it has. It takes effect when the
|
|
47
|
+
* bridge starts opencode's server; a server already running in the sandbox keeps the one it
|
|
48
|
+
* started with.
|
|
49
|
+
*/
|
|
50
|
+
readonly config?: {
|
|
51
|
+
readonly [key: string]: Schema.Json;
|
|
52
|
+
};
|
|
53
|
+
/** Environment variables opencode's server runs with. */
|
|
54
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The agent, which bundles the bridge program for its template when it is made, with `Bun.build`,
|
|
58
|
+
* so the host runs on Bun.
|
|
59
|
+
*
|
|
60
|
+
* **Details**
|
|
61
|
+
*
|
|
62
|
+
* The names of the host's tools fit in 59 characters, as `maxToolNameLength` says, since opencode
|
|
63
|
+
* puts `host_` before them. Each session reaches opencode through the bridge in its sandbox, with
|
|
64
|
+
* the `Crypto` and the `WebSocketConstructor` the agent is made with; the constructor must send
|
|
65
|
+
* headers, as Bun's and Node's do. Endpoints that opencode cannot serve together fail with
|
|
66
|
+
* `UnsupportedError`.
|
|
67
|
+
*
|
|
68
|
+
* **Example** (A turn of opencode in a new sandbox)
|
|
69
|
+
*
|
|
70
|
+
* ```ts
|
|
71
|
+
* import { createHmac } from "node:crypto"
|
|
72
|
+
* import { Agent, Harness } from "@amends-dev/harness"
|
|
73
|
+
* import { OpencodeAgent } from "@amends-dev/harness-opencode"
|
|
74
|
+
* import { SandboxProvider } from "@amends-dev/sandbox"
|
|
75
|
+
* import { BunServices, BunSocket } from "@effect/platform-bun"
|
|
76
|
+
* import { Config, Effect, Layer, Redacted } from "effect"
|
|
77
|
+
*
|
|
78
|
+
* declare const sandboxes: Layer.Layer<SandboxProvider.SandboxProvider> // a sandbox provider
|
|
79
|
+
*
|
|
80
|
+
* const program = Effect.gen(function* () {
|
|
81
|
+
* // Every host derives a sandbox's bridge token from its id with a secret the hosts share.
|
|
82
|
+
* const secret = yield* Config.Redacted("BRIDGE_SECRET")
|
|
83
|
+
* const agent = yield* OpencodeAgent.make({
|
|
84
|
+
* model: {
|
|
85
|
+
* provider: "anthropic",
|
|
86
|
+
* model: "claude-sonnet-5-5",
|
|
87
|
+
* protocol: "anthropic-messages",
|
|
88
|
+
* url: "https://api.anthropic.com/v1",
|
|
89
|
+
* apiKey: yield* Config.Redacted("ANTHROPIC_API_KEY")
|
|
90
|
+
* },
|
|
91
|
+
* token: (sandboxId) =>
|
|
92
|
+
* Redacted.make(createHmac("sha256", Redacted.value(secret)).update(sandboxId).digest("base64url"))
|
|
93
|
+
* })
|
|
94
|
+
* const harness = Harness.make(agent)
|
|
95
|
+
* const sandboxProvider = yield* SandboxProvider.SandboxProvider
|
|
96
|
+
* const sandbox = yield* sandboxProvider.createScoped({ template: harness.agent.template })
|
|
97
|
+
* const session = yield* harness.createSession({ sandbox, id: Agent.SessionId.make("review") })
|
|
98
|
+
* const answer = yield* session.generate({ prompt: "Summarize README.md" })
|
|
99
|
+
* return answer.text
|
|
100
|
+
* }).pipe(
|
|
101
|
+
* Effect.scoped,
|
|
102
|
+
* Effect.provide(Layer.mergeAll(sandboxes, BunServices.layer, BunSocket.layerWebSocketConstructor))
|
|
103
|
+
* )
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* @category constructors
|
|
107
|
+
* @since 0.1.0
|
|
108
|
+
*/
|
|
109
|
+
export declare const make: (options: Options) => import("effect/Effect").Effect<Agent.Agent, import("@amends-dev/harness/HarnessError").HarnessError | import("effect/PlatformError").PlatformError, import("effect/Crypto").Crypto | import("effect/socket/Socket").WebSocketConstructor>;
|
|
110
|
+
//# sourceMappingURL=OpencodeAgent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OpencodeAgent.d.ts","sourceRoot":"","sources":["../src/OpencodeAgent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,KAAK,QAAQ,MAAM,iBAAiB,CAAA;AAChD,OAAO,KAAK,KAAK,MAAM,MAAM,eAAe,CAAA;AAI5C;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,OAAQ,SAAQ,KAAK,CAAC,OAAO;IAC5C;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,OAAO,CAAC,SAAS,KAAK,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAA;IAEpE;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,IAAI,CAAA;KAAE,CAAA;IAEzD,yDAAyD;IACzD,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CAChD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,eAAO,MAAM,IAAI,YAAa,OAAO,8OASlC,CAAA"}
|
package/OpencodeAgent.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opencode as an agent that hosts run: opencode 1, driven over its session API, which takes all of
|
|
3
|
+
* a turn: its instructions, as the system prompt of its prompt, an answer in JSON, the files of its
|
|
4
|
+
* prompt, the host's tools, and requests to compact. opencode works in the sandbox behind its
|
|
5
|
+
* bridge: the agent's template installs both there, and a session launches the bridge when none
|
|
6
|
+
* answers. One bridge and one opencode server serve every session that hosts start in a sandbox
|
|
7
|
+
* with the same options, whichever host started it; other options, such as another model, get a
|
|
8
|
+
* bridge and a server of their own. What runs in the sandbox is in `internal/bridge/v1/`.
|
|
9
|
+
*
|
|
10
|
+
* opencode runs on the model endpoints the agent is made with, which it is configured for.
|
|
11
|
+
* Their keys reach opencode in its configuration, in the sandbox, where the commands opencode runs
|
|
12
|
+
* can read them. The host's tools reach opencode as an MCP server the bridge serves on the
|
|
13
|
+
* sandbox's loopback, whose tools opencode names `host_<tool>`; the host runs them, and nothing
|
|
14
|
+
* they need reaches the sandbox.
|
|
15
|
+
*
|
|
16
|
+
* A session started on another agent is written into opencode as one message the model reads,
|
|
17
|
+
* since opencode 1 records no steps but its own. `Opencode2Agent`, over opencode 2's session API,
|
|
18
|
+
* records them as opencode's own, but does not answer in JSON yet.
|
|
19
|
+
*
|
|
20
|
+
* @since 0.1.0
|
|
21
|
+
*/
|
|
22
|
+
import * as internal from "./internal/agent.js";
|
|
23
|
+
import { hostToolName } from "./internal/bridge/v1/session.js";
|
|
24
|
+
/**
|
|
25
|
+
* The agent, which bundles the bridge program for its template when it is made, with `Bun.build`,
|
|
26
|
+
* so the host runs on Bun.
|
|
27
|
+
*
|
|
28
|
+
* **Details**
|
|
29
|
+
*
|
|
30
|
+
* The names of the host's tools fit in 59 characters, as `maxToolNameLength` says, since opencode
|
|
31
|
+
* puts `host_` before them. Each session reaches opencode through the bridge in its sandbox, with
|
|
32
|
+
* the `Crypto` and the `WebSocketConstructor` the agent is made with; the constructor must send
|
|
33
|
+
* headers, as Bun's and Node's do. Endpoints that opencode cannot serve together fail with
|
|
34
|
+
* `UnsupportedError`.
|
|
35
|
+
*
|
|
36
|
+
* **Example** (A turn of opencode in a new sandbox)
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* import { createHmac } from "node:crypto"
|
|
40
|
+
* import { Agent, Harness } from "@amends-dev/harness"
|
|
41
|
+
* import { OpencodeAgent } from "@amends-dev/harness-opencode"
|
|
42
|
+
* import { SandboxProvider } from "@amends-dev/sandbox"
|
|
43
|
+
* import { BunServices, BunSocket } from "@effect/platform-bun"
|
|
44
|
+
* import { Config, Effect, Layer, Redacted } from "effect"
|
|
45
|
+
*
|
|
46
|
+
* declare const sandboxes: Layer.Layer<SandboxProvider.SandboxProvider> // a sandbox provider
|
|
47
|
+
*
|
|
48
|
+
* const program = Effect.gen(function* () {
|
|
49
|
+
* // Every host derives a sandbox's bridge token from its id with a secret the hosts share.
|
|
50
|
+
* const secret = yield* Config.Redacted("BRIDGE_SECRET")
|
|
51
|
+
* const agent = yield* OpencodeAgent.make({
|
|
52
|
+
* model: {
|
|
53
|
+
* provider: "anthropic",
|
|
54
|
+
* model: "claude-sonnet-5-5",
|
|
55
|
+
* protocol: "anthropic-messages",
|
|
56
|
+
* url: "https://api.anthropic.com/v1",
|
|
57
|
+
* apiKey: yield* Config.Redacted("ANTHROPIC_API_KEY")
|
|
58
|
+
* },
|
|
59
|
+
* token: (sandboxId) =>
|
|
60
|
+
* Redacted.make(createHmac("sha256", Redacted.value(secret)).update(sandboxId).digest("base64url"))
|
|
61
|
+
* })
|
|
62
|
+
* const harness = Harness.make(agent)
|
|
63
|
+
* const sandboxProvider = yield* SandboxProvider.SandboxProvider
|
|
64
|
+
* const sandbox = yield* sandboxProvider.createScoped({ template: harness.agent.template })
|
|
65
|
+
* const session = yield* harness.createSession({ sandbox, id: Agent.SessionId.make("review") })
|
|
66
|
+
* const answer = yield* session.generate({ prompt: "Summarize README.md" })
|
|
67
|
+
* return answer.text
|
|
68
|
+
* }).pipe(
|
|
69
|
+
* Effect.scoped,
|
|
70
|
+
* Effect.provide(Layer.mergeAll(sandboxes, BunServices.layer, BunSocket.layerWebSocketConstructor))
|
|
71
|
+
* )
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* @category constructors
|
|
75
|
+
* @since 0.1.0
|
|
76
|
+
*/
|
|
77
|
+
export const make = (options) => internal.make({
|
|
78
|
+
id: "opencode",
|
|
79
|
+
version: 1,
|
|
80
|
+
// opencode names a host's tool after the MCP server that offers it, `host_<tool>`.
|
|
81
|
+
maxToolNameLength: 64 - hostToolName("").length
|
|
82
|
+
}, options);
|
|
83
|
+
//# sourceMappingURL=OpencodeAgent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OpencodeAgent.js","sourceRoot":"","sources":["../src/OpencodeAgent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAMH,OAAO,KAAK,QAAQ,MAAM,qBAAqB,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAA;AAiC9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,OAAgB,EAAE,EAAE,CACvC,QAAQ,CAAC,IAAI,CACX;IACE,EAAE,EAAE,UAAU;IACd,OAAO,EAAE,CAAC;IACV,mFAAmF;IACnF,iBAAiB,EAAE,EAAE,GAAG,YAAY,CAAC,EAAE,CAAC,CAAC,MAAM;CAChD,EACD,OAAO,CACR,CAAA"}
|
package/README.md
CHANGED
|
@@ -1,3 +1,222 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @amends-dev/harness-opencode
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
opencode as an agent for the [`@amends-dev/harness`](../../packages/harness) contract. opencode
|
|
4
|
+
works in the sandbox behind its bridge, a program the agent's template installs there with opencode
|
|
5
|
+
itself: the bridge drives opencode's server, and hosts reach the bridge over a WebSocket on a port
|
|
6
|
+
of the sandbox's. Nothing else of opencode is exposed outside the sandbox, and hosts that start
|
|
7
|
+
sessions in a sandbox with the same options share its bridge and its server, whichever host
|
|
8
|
+
launched them.
|
|
9
|
+
|
|
10
|
+
opencode has two versions, each with a session API of its own, and the package has an agent for
|
|
11
|
+
each:
|
|
12
|
+
|
|
13
|
+
| Agent | Runs | Takes |
|
|
14
|
+
| ---------------- | ------------------- | ------------------------------------------------------------------------ |
|
|
15
|
+
| `OpencodeAgent` | opencode 1, 1.18.23 | all of a turn |
|
|
16
|
+
| `Opencode2Agent` | opencode 2, 2.0.26 | all of a turn but an answer in JSON; records other agents' conversations |
|
|
17
|
+
|
|
18
|
+
opencode 1 is the opencode most people run, from the npm package `opencode-ai`, and opencode 2 is
|
|
19
|
+
its successor, from `@opencode/cli`, whose session API is still taking shape: `Opencode2Agent`
|
|
20
|
+
follows it, taking more of a turn as opencode 2 does. Both agents give a turn its instructions,
|
|
21
|
+
files, and the host's tools, ask the host about the permissions opencode's tools ask, and compact on
|
|
22
|
+
request; they share everything but the opencode they run and the session API they drive.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
bun add @amends-dev/harness-opencode @amends-dev/harness @amends-dev/sandbox effect
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Add a sandbox provider, such as [`@amends-dev/sandbox-local`](../sandbox-local) for this machine or
|
|
31
|
+
[`@amends-dev/sandbox-daytona`](../sandbox-daytona). The host runs on Bun: `OpencodeAgent.make`
|
|
32
|
+
bundles the bridge program with `Bun.build`. It needs Effect's `Crypto` and a
|
|
33
|
+
`Socket.WebSocketConstructor` that sends headers, as `@effect/platform-bun` provides them.
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
`OpencodeAgent.make` makes the agent on a model endpoint and a `token` function, which derives
|
|
38
|
+
each sandbox's bridge token from its id with a secret every host shares, so that every host
|
|
39
|
+
connects to a bridge any of them launched. The caller creates each sandbox with the agent's
|
|
40
|
+
`template`, which installs opencode and the bridge, and `Harness.make` runs the agent in it.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { OpencodeAgent } from "@amends-dev/harness-opencode"
|
|
44
|
+
import { Agent, Harness } from "@amends-dev/harness"
|
|
45
|
+
import { SandboxProvider } from "@amends-dev/sandbox"
|
|
46
|
+
import { BunServices, BunSocket } from "@effect/platform-bun"
|
|
47
|
+
import { Config, Effect, Layer, Redacted } from "effect"
|
|
48
|
+
import { createHmac } from "node:crypto"
|
|
49
|
+
|
|
50
|
+
declare const sandboxes: Layer.Layer<SandboxProvider.SandboxProvider> // a sandbox provider
|
|
51
|
+
|
|
52
|
+
const program = Effect.gen(function* () {
|
|
53
|
+
// Every host derives a sandbox's bridge token from its id with a secret the hosts share.
|
|
54
|
+
const secret = yield* Config.Redacted("BRIDGE_SECRET")
|
|
55
|
+
const agent = yield* OpencodeAgent.make({
|
|
56
|
+
model: {
|
|
57
|
+
provider: "anthropic",
|
|
58
|
+
model: "claude-sonnet-5-5",
|
|
59
|
+
protocol: "anthropic-messages",
|
|
60
|
+
url: "https://api.anthropic.com/v1",
|
|
61
|
+
apiKey: yield* Config.Redacted("ANTHROPIC_API_KEY")
|
|
62
|
+
},
|
|
63
|
+
token: (sandboxId) =>
|
|
64
|
+
Redacted.make(createHmac("sha256", Redacted.value(secret)).update(sandboxId).digest("base64url"))
|
|
65
|
+
})
|
|
66
|
+
const harness = Harness.make(agent)
|
|
67
|
+
const sandboxProvider = yield* SandboxProvider.SandboxProvider
|
|
68
|
+
const sandbox = yield* sandboxProvider.createScoped({ template: harness.agent.template })
|
|
69
|
+
const session = yield* harness.createSession({ sandbox, id: Agent.SessionId.make("review") })
|
|
70
|
+
const answer = yield* session.generate({ prompt: "Summarize README.md" })
|
|
71
|
+
return answer.text
|
|
72
|
+
}).pipe(
|
|
73
|
+
Effect.scoped,
|
|
74
|
+
Effect.provide(Layer.mergeAll(sandboxes, BunServices.layer, BunSocket.layerWebSocketConstructor))
|
|
75
|
+
)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`config` is opencode's configuration, as its `opencode.json` takes it, whose providers the models'
|
|
79
|
+
replace: `provider` for `OpencodeAgent`, as opencode 1 names them, and `providers` for
|
|
80
|
+
`Opencode2Agent`, as opencode 2 does. `env` is the environment variables opencode's server runs
|
|
81
|
+
with. Both take effect when the bridge starts the server. All of the options, the keys included,
|
|
82
|
+
name the bridge, so an agent made with other options gets a bridge and a server of its own in a
|
|
83
|
+
sandbox, and never runs on another's. [`@amends-dev/harness`](../../packages/harness/README.md) says
|
|
84
|
+
how sessions, turns, and releasing a session work, which these agents do as any agent does.
|
|
85
|
+
`Opencode2Agent.make` takes the same options.
|
|
86
|
+
|
|
87
|
+
## In the sandbox
|
|
88
|
+
|
|
89
|
+
Each agent's template installs Bun 1.3.14 and its opencode in the sandbox's home, under
|
|
90
|
+
`~/.amends/opencode/<programs>`, a directory named for their versions: opencode 1.18.23 from
|
|
91
|
+
`opencode-<platform>` for `OpencodeAgent`, and opencode 2.0.26 from `@opencode/cli-<platform>` for
|
|
92
|
+
`Opencode2Agent`. Each comes from its npm package for the sandbox's platform, macOS or Linux with
|
|
93
|
+
glibc on arm64 or x64, which the sandbox downloads from npm and checks against the integrity pinned
|
|
94
|
+
in the package. The agent's bridge program goes beside them as `bridge-<bundle>.js`, named for its
|
|
95
|
+
contents. Preparing a sandbox needs `sh`, `uname`, `curl`, `tar`, `sha512sum` or `shasum`, and
|
|
96
|
+
access to `registry.npmjs.org`. What the sandbox has already is kept: a sandbox whose home has the
|
|
97
|
+
programs, such as every local sandbox after the first on this machine, gets only a new bridge
|
|
98
|
+
program, and a sandbox from a snapshot of a prepared one is ready at once. The template's identity
|
|
99
|
+
changes with the versions and the bundle; the programs and bridge programs of versions no host
|
|
100
|
+
launches any more are not removed.
|
|
101
|
+
|
|
102
|
+
A session launches the bridge, when none answers, with
|
|
103
|
+
`sh -c 'exec "$HOME/…/bun" "$HOME/…/bridge-<bundle>.js"'`, giving it its launch options on
|
|
104
|
+
standard input. It runs the template first, so that a sandbox a host of another version prepared
|
|
105
|
+
gets this version's bridge program, and Bun and opencode if their versions changed. The bridge
|
|
106
|
+
serves on a port the sandbox picks, which it writes to its state directory for hosts to find, and
|
|
107
|
+
exits once it has been idle for ten minutes or another bridge takes its place.
|
|
108
|
+
|
|
109
|
+
When the first session starts, the bridge starts opencode's server, `opencode serve`, on
|
|
110
|
+
`127.0.0.1` at a free port it picks, behind a password it makes for that start and gives the
|
|
111
|
+
server only in its environment. The server reads its configuration from a directory the bridge
|
|
112
|
+
makes for it, which only the sandbox's user can read and which the bridge removes when the server
|
|
113
|
+
stops, in place of the configuration under the sandbox user's home; a project's own configuration,
|
|
114
|
+
in a session's directory, applies over it. A server that has not stopped 3 seconds after the
|
|
115
|
+
bridge asks it to is killed. One server serves every session of the bridge, each in its own
|
|
116
|
+
directory. A server that fails to start is started again by the next session; one that exits by
|
|
117
|
+
itself takes the bridge with it, and the next session launches both again. A bridge that dies, as
|
|
118
|
+
when it is killed, leaves its server running, still on the turn the bridge ran, beside the server
|
|
119
|
+
the next bridge starts, and its configuration stays with it. The bridge also serves the host's
|
|
120
|
+
tools to opencode as an MCP server on `127.0.0.1`, behind a token for each session. opencode keeps
|
|
121
|
+
its data where it would for any user of the sandbox, under its home, unless `env` moves it, as with
|
|
122
|
+
`XDG_DATA_HOME`.
|
|
123
|
+
|
|
124
|
+
## Models
|
|
125
|
+
|
|
126
|
+
The agents configure opencode for their endpoints. Each endpoint names its provider and model as
|
|
127
|
+
opencode finds them in models.dev, which tells opencode the rest of the model, such as whether it
|
|
128
|
+
calls tools. opencode takes a model that models.dev does not list as one that calls tools but reads
|
|
129
|
+
no files. The configuration serves the model through the endpoint's protocol, at its URL, within its
|
|
130
|
+
limits, and under its key and headers, and asks for the endpoint's effort in a variant of the model,
|
|
131
|
+
which the turn's model names. opencode presents one key, with one set of headers, for each provider
|
|
132
|
+
and serves a model one way, so endpoints that differ there fail `make` with `UnsupportedError`. The
|
|
133
|
+
keys and headers reach opencode in its configuration in the sandbox, where the commands opencode
|
|
134
|
+
runs can read them.
|
|
135
|
+
|
|
136
|
+
opencode 1 takes a model's output limit only with its context window. An endpoint with an output
|
|
137
|
+
limit gives its context window too, or 0 when it has none, which opencode takes as unknown and
|
|
138
|
+
compacts nothing by itself. An endpoint without an output limit leaves both limits to models.dev.
|
|
139
|
+
opencode 1 writes at most 32,000 tokens in a response unless its server is told otherwise, so
|
|
140
|
+
`OpencodeAgent` raises that ceiling to the highest output limit of its endpoints. opencode 2 takes
|
|
141
|
+
each limit apart, over what models.dev says, and writes as many tokens as the output limit allows.
|
|
142
|
+
|
|
143
|
+
## Sessions and turns
|
|
144
|
+
|
|
145
|
+
Each session is an opencode session, and the agent's state names it. A session whose opencode
|
|
146
|
+
session is still there continues it; one that starts in a sandbox whose opencode has no record of
|
|
147
|
+
it, such as one that started on another agent, starts a new opencode session from the
|
|
148
|
+
conversation. Destroying a session deletes it in opencode, and a turn the bridge aborts is
|
|
149
|
+
interrupted there.
|
|
150
|
+
|
|
151
|
+
### `OpencodeAgent`
|
|
152
|
+
|
|
153
|
+
A turn is a prompt to the session, on the model the turn names, with the turn's instructions as
|
|
154
|
+
its system prompt, the schema of an answer in JSON, and the prompt's files, which reach opencode
|
|
155
|
+
as data URLs or as the URLs they are. The events opencode publishes for the turn become the
|
|
156
|
+
harness's stream parts as they happen: text and reasoning stream as opencode writes them, a tool's
|
|
157
|
+
input streams whole once the tool runs, and opencode's own tools run inside opencode, so their
|
|
158
|
+
calls and results are `providerExecuted`. A file that opencode's `write` or `edit` tool changes is
|
|
159
|
+
a `file-change` part, with its path relative to the session's directory. An answer in JSON is the
|
|
160
|
+
text of the last step.
|
|
161
|
+
|
|
162
|
+
The host's tools reach opencode through the bridge's MCP server, which opencode names `host`, so
|
|
163
|
+
it calls a tool `host_<tool>`: the names of the host's tools fit in 59 characters, as the agent's
|
|
164
|
+
`maxToolNameLength` says. The calls stream as the host's, which the host answers, and opencode
|
|
165
|
+
lists the tools again when a turn's tools change. A tool of opencode's own that asks permission,
|
|
166
|
+
as opencode's `permission` configuration says, streams a `tool-approval-request` and waits for
|
|
167
|
+
the host's answer. opencode does not ask the user questions in a turn: its `question` tool is off,
|
|
168
|
+
and a question asked anyway is refused.
|
|
169
|
+
|
|
170
|
+
`compact` asks opencode to summarize the conversation, on the model of its last step, and returns
|
|
171
|
+
the summary; opencode summarizes without instructions, so instructions fail with
|
|
172
|
+
`UnsupportedError`. opencode also compacts by itself when a turn nears the model's context window,
|
|
173
|
+
which streams as a `compaction` part. A conversation from another agent is written into the new
|
|
174
|
+
opencode session as one message the model reads with the first prompt, the files of its user
|
|
175
|
+
messages beside it, since opencode 1 records no steps but its own.
|
|
176
|
+
|
|
177
|
+
### `Opencode2Agent`
|
|
178
|
+
|
|
179
|
+
A turn is a prompt to the session, with the prompt's files as `data:` URLs, after the session is
|
|
180
|
+
given the turn's model and instructions: opencode 2 keeps instructions as an entry of the session's
|
|
181
|
+
own, which the model reads beside opencode's, and which the agent replaces or removes when a turn
|
|
182
|
+
gives others or none. The events opencode publishes for the turn become the harness's stream parts
|
|
183
|
+
as they happen, as for `OpencodeAgent`: text, reasoning, and a tool's input stream as opencode
|
|
184
|
+
writes them, opencode's own tools are `providerExecuted`, and a file that opencode's `write`,
|
|
185
|
+
`edit`, or `patch` tool changes is a `file-change` part.
|
|
186
|
+
|
|
187
|
+
The host's tools reach opencode 2 through the same MCP server, which opencode 2's model calls from
|
|
188
|
+
code it runs, in its own tool, before the relay streams the call as the host's. When a turn's tools
|
|
189
|
+
change, it waits until opencode has connected to the server, and then half a second more: opencode 2
|
|
190
|
+
gives its turns a server's new tools 100 milliseconds after they change, and does not say when it
|
|
191
|
+
has. A tool that asks permission, as opencode's `permissions` configuration says, streams a
|
|
192
|
+
`tool-approval-request`, and a form the model asks the user to fill is cancelled. `compact` asks
|
|
193
|
+
opencode to summarize the conversation and returns the summary, without instructions. A
|
|
194
|
+
conversation from another agent is imported as the new session's own record, tool calls included,
|
|
195
|
+
with the files of its user messages.
|
|
196
|
+
|
|
197
|
+
opencode 2's session API does not answer in JSON yet, which fails with `UnsupportedError`, and reads
|
|
198
|
+
a prompt's files only as data, so a file at an `http` URL fails the same way. It does not keep a
|
|
199
|
+
session's events where a client can read them again, so a turn the bridge did not finish is not
|
|
200
|
+
continued from where opencode got to.
|
|
201
|
+
|
|
202
|
+
## Errors
|
|
203
|
+
|
|
204
|
+
The agents' operations fail with `HarnessError`, naming the agent `opencode` or `opencode2`:
|
|
205
|
+
`UnsupportedError` for what opencode cannot do, a turn that names a model the agent has none of, or
|
|
206
|
+
endpoints opencode cannot serve together; `UnavailableError` when its server cannot be reached,
|
|
207
|
+
exits, does not listen in time, asks to be retried, or does not connect to the host's tools;
|
|
208
|
+
`NotFoundError` for what the server has no record of; `AgentContractError` for state or events the
|
|
209
|
+
agent cannot read; and `UnknownError` for the rest.
|
|
210
|
+
|
|
211
|
+
## Development
|
|
212
|
+
|
|
213
|
+
Run the package's tests with `bun run test adapters/harness-opencode`: each agent's sessions against
|
|
214
|
+
a fake of opencode's server, scripted turn by turn, and the translation of its events and
|
|
215
|
+
conversations, under `test/v1` and `test/v2`; the server and the bridge program as real processes,
|
|
216
|
+
with `test/fake-opencode-server.js` standing in for `opencode serve`; the bridge launched by
|
|
217
|
+
`BridgeClient` in a sandbox on this machine and reached over a WebSocket; and the template with its
|
|
218
|
+
commands answered as a sandbox would answer them. With `AMENDS_TEST_OPENCODE=1`, the live tests run
|
|
219
|
+
`opencode` and `opencode2` from this machine's `PATH` under the agents, whose turns OpenCode Zen's
|
|
220
|
+
free model answers. See [testing](../../docs/testing.md).
|
|
221
|
+
`bun run dist check adapters/harness-opencode` builds the package as npm gets it and checks it as a
|
|
222
|
+
consumer; see [publishing](../../docs/publishing.md).
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opencode as an agent for the `@amends-dev/harness` contract, working in a sandbox behind its
|
|
3
|
+
* bridge, which the agent's template installs there with opencode itself. opencode has two
|
|
4
|
+
* versions, each with a session API of its own, and the package an agent for each: `OpencodeAgent`
|
|
5
|
+
* runs opencode 1, which takes everything a turn gives, and `Opencode2Agent` runs opencode 2, which
|
|
6
|
+
* records a session started on another agent as its own but does not answer in JSON yet.
|
|
7
|
+
*
|
|
8
|
+
* @since 0.1.0
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* @since 0.1.0
|
|
12
|
+
*/
|
|
13
|
+
export * as Opencode2Agent from "./Opencode2Agent.ts";
|
|
14
|
+
/**
|
|
15
|
+
* @since 0.1.0
|
|
16
|
+
*/
|
|
17
|
+
export * as OpencodeAgent from "./OpencodeAgent.ts";
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|