@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.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/Opencode2Agent.d.ts +105 -0
  3. package/Opencode2Agent.d.ts.map +1 -0
  4. package/Opencode2Agent.js +72 -0
  5. package/Opencode2Agent.js.map +1 -0
  6. package/OpencodeAgent.d.ts +110 -0
  7. package/OpencodeAgent.d.ts.map +1 -0
  8. package/OpencodeAgent.js +83 -0
  9. package/OpencodeAgent.js.map +1 -0
  10. package/README.md +221 -2
  11. package/index.d.ts +18 -0
  12. package/index.d.ts.map +1 -0
  13. package/index.js +19 -0
  14. package/index.js.map +1 -0
  15. package/internal/agent.d.ts +29 -0
  16. package/internal/agent.d.ts.map +1 -0
  17. package/internal/agent.js +34 -0
  18. package/internal/agent.js.map +1 -0
  19. package/internal/bridge/bunBridge.d.ts +33 -0
  20. package/internal/bridge/bunBridge.d.ts.map +1 -0
  21. package/internal/bridge/bunBridge.js +62 -0
  22. package/internal/bridge/bunBridge.js.map +1 -0
  23. package/internal/bridge/driver.d.ts +81 -0
  24. package/internal/bridge/driver.d.ts.map +1 -0
  25. package/internal/bridge/driver.js +82 -0
  26. package/internal/bridge/driver.js.map +1 -0
  27. package/internal/bridge/hostTools.d.ts +28 -0
  28. package/internal/bridge/hostTools.d.ts.map +1 -0
  29. package/internal/bridge/hostTools.js +41 -0
  30. package/internal/bridge/hostTools.js.map +1 -0
  31. package/internal/bridge/server.d.ts +64 -0
  32. package/internal/bridge/server.d.ts.map +1 -0
  33. package/internal/bridge/server.js +129 -0
  34. package/internal/bridge/server.js.map +1 -0
  35. package/internal/bridge/steps.d.ts +29 -0
  36. package/internal/bridge/steps.d.ts.map +1 -0
  37. package/internal/bridge/steps.js +42 -0
  38. package/internal/bridge/steps.js.map +1 -0
  39. package/internal/bridge/v1/api.d.ts +181 -0
  40. package/internal/bridge/v1/api.d.ts.map +1 -0
  41. package/internal/bridge/v1/api.js +83 -0
  42. package/internal/bridge/v1/api.js.map +1 -0
  43. package/internal/bridge/v1/event.d.ts +597 -0
  44. package/internal/bridge/v1/event.d.ts.map +1 -0
  45. package/internal/bridge/v1/event.js +186 -0
  46. package/internal/bridge/v1/event.js.map +1 -0
  47. package/internal/bridge/v1/main.d.ts +6 -0
  48. package/internal/bridge/v1/main.d.ts.map +1 -0
  49. package/internal/bridge/v1/main.js +8 -0
  50. package/internal/bridge/v1/main.js.map +1 -0
  51. package/internal/bridge/v1/prompt.d.ts +18 -0
  52. package/internal/bridge/v1/prompt.d.ts.map +1 -0
  53. package/internal/bridge/v1/prompt.js +92 -0
  54. package/internal/bridge/v1/prompt.js.map +1 -0
  55. package/internal/bridge/v1/session.d.ts +28 -0
  56. package/internal/bridge/v1/session.d.ts.map +1 -0
  57. package/internal/bridge/v1/session.js +188 -0
  58. package/internal/bridge/v1/session.js.map +1 -0
  59. package/internal/bridge/v1/turn.d.ts +82 -0
  60. package/internal/bridge/v1/turn.d.ts.map +1 -0
  61. package/internal/bridge/v1/turn.js +304 -0
  62. package/internal/bridge/v1/turn.js.map +1 -0
  63. package/internal/bridge/v2/api.d.ts +86 -0
  64. package/internal/bridge/v2/api.d.ts.map +1 -0
  65. package/internal/bridge/v2/api.js +105 -0
  66. package/internal/bridge/v2/api.js.map +1 -0
  67. package/internal/bridge/v2/event.d.ts +539 -0
  68. package/internal/bridge/v2/event.d.ts.map +1 -0
  69. package/internal/bridge/v2/event.js +164 -0
  70. package/internal/bridge/v2/event.js.map +1 -0
  71. package/internal/bridge/v2/main.d.ts +6 -0
  72. package/internal/bridge/v2/main.d.ts.map +1 -0
  73. package/internal/bridge/v2/main.js +8 -0
  74. package/internal/bridge/v2/main.js.map +1 -0
  75. package/internal/bridge/v2/prompt.d.ts +109 -0
  76. package/internal/bridge/v2/prompt.d.ts.map +1 -0
  77. package/internal/bridge/v2/prompt.js +209 -0
  78. package/internal/bridge/v2/prompt.js.map +1 -0
  79. package/internal/bridge/v2/session.d.ts +21 -0
  80. package/internal/bridge/v2/session.d.ts.map +1 -0
  81. package/internal/bridge/v2/session.js +198 -0
  82. package/internal/bridge/v2/session.js.map +1 -0
  83. package/internal/bridge/v2/turn.d.ts +62 -0
  84. package/internal/bridge/v2/turn.d.ts.map +1 -0
  85. package/internal/bridge/v2/turn.js +277 -0
  86. package/internal/bridge/v2/turn.js.map +1 -0
  87. package/internal/models.d.ts +47 -0
  88. package/internal/models.d.ts.map +1 -0
  89. package/internal/models.js +170 -0
  90. package/internal/models.js.map +1 -0
  91. package/internal/template.d.ts +46 -0
  92. package/internal/template.d.ts.map +1 -0
  93. package/internal/template.js +267 -0
  94. package/internal/template.js.map +1 -0
  95. 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"}
@@ -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
- # Temporary Holding Version
1
+ # @amends-dev/harness-opencode
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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