@dudousxd/nestjs-agent-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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Davide Carvalho
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.
package/README.md CHANGED
@@ -1,3 +1,146 @@
1
- # Temporary Holding Version
1
+ # @dudousxd/nestjs-agent-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
+ Run `@dudousxd/nestjs-agent` turns on [OpenCode 2](https://opencode.ai). The library keeps the
4
+ routes, threads, stream protocol, approvals, questions and queue (so `@dudousxd/nestjs-agent-react`
5
+ works unchanged); OpenCode runs the model, the tools, skills and the context.
6
+
7
+ ```ts
8
+ import { AgentModule } from '@dudousxd/nestjs-agent';
9
+ import { openCode, type OpenCodeHost } from '@dudousxd/nestjs-agent-opencode';
10
+
11
+ @Injectable()
12
+ class MyOpenCodeHost implements OpenCodeHost {
13
+ constructor(private readonly sandboxes: SandboxService) {}
14
+
15
+ /** Which OpenCode server runs this actor's turns. */
16
+ async server(actor: Actor) {
17
+ const rt = await this.sandboxes.runtimeFor(actor.tenantId);
18
+ return { client: rt.client, key: actor.tenantId, bootId: rt.bootId };
19
+ }
20
+
21
+ /** How a thread's session is created (only when it has none on this server). */
22
+ async session({ input }: OpenCodeTurnContext) {
23
+ return {
24
+ location: { directory: `/work/${input.actor.id}` },
25
+ permissions: [
26
+ { action: '*', resource: '*', effect: 'deny' },
27
+ { action: 'execute', resource: '*', effect: 'allow' },
28
+ { action: 'company.*send*', resource: '*', effect: 'ask' }, // → approval card
29
+ { action: 'question', resource: '*', effect: 'allow' }, // → elicitation form
30
+ ],
31
+ };
32
+ }
33
+
34
+ /** Extra instructions entries, refreshed every turn. */
35
+ async instructions({ input }: OpenCodeTurnContext) {
36
+ return { 'app.profile': await this.profiles.describe(input.actor) };
37
+ }
38
+ }
39
+
40
+ @Module({
41
+ imports: [
42
+ AgentModule.forRoot({
43
+ engine: openCode({ host: MyOpenCodeHost }),
44
+ store, // any AgentStore
45
+ actorResolver,
46
+ }),
47
+ ],
48
+ providers: [MyAgent], // @Agent({ systemPrompt }) / @SystemPrompt() → instructions `aviary.system`
49
+ })
50
+ export class AppModule {}
51
+ ```
52
+
53
+ | OpenCode | The library's protocol and store |
54
+ | --- | --- |
55
+ | `session.text.delta` / `session.reasoning.delta` | `text` / `reasoning` |
56
+ | a step (`session.step.ended`) | `step-start` … `step-finish` with usage; usage recorded per step |
57
+ | tools (`session.tool.*`) | `tool-input-*` / `tool-output*`, code-mode inner calls nested by `parentId` |
58
+ | `permission.asked` | an `action` call + `approval-requested`, recorded `pending_approval`; `approve` / `reject` → `permission.reply` |
59
+ | `form.created` | `elicitation`; `answer` / `skip` → `session.form.reply` / `cancel` |
60
+ | `session.renamed` | `title` |
61
+ | `session.execution.*` | the run settles: queue handoff, `cancelled`, or a typed stream failure |
62
+ | `cancel` | `session.interrupt` |
63
+
64
+ Sessions: one per thread, kept by an `OpenCodeSessionStore` (in memory by default; persist it for
65
+ several replicas) and recreated when the server's `bootId` changes, told the conversation so far.
66
+
67
+ ## Durable turns
68
+
69
+ ```ts
70
+ import { openCodeDurable } from '@dudousxd/nestjs-agent-opencode/durable';
71
+
72
+ AgentModule.forRoot({ engine: openCodeDurable({ host: MyOpenCodeHost, sessions: MySessionStore }), store, sink, actorResolver })
73
+ // next to a configured DurableModule
74
+ ```
75
+
76
+ Every step of a turn is checkpointed (`begin → prompt → observe → [wait for a person → reply →
77
+ observe]* → finish`) and a person's decision is a durable signal: a turn waiting on an approval
78
+ survives restarts and is resumed by whichever process gets the decision. If OpenCode restarted
79
+ meanwhile, a new session is opened with the conversation and the decision. `openCode()` runs the
80
+ same steps in memory (single replica). Several processes need a cross-process sink and a persistent
81
+ `OpenCodeSessionStore`.
82
+
83
+ ## What the session gets from the module
84
+
85
+ | Option | In the session |
86
+ | --- | --- |
87
+ | `@Agent` / `@SystemPrompt` / contributors | instructions `aviary.system`, refreshed every turn |
88
+ | `approvalPolicy` | who approves each `permission.asked`, and its expiry; not required → answered at once |
89
+ | `tools: { url, secret }` | the module's `@AiTool`s over the engine's own MCP endpoint: reads allowed, actions asked, and run only against an approval |
90
+ | `skills` / `@Skill` | `.opencode/skills/<name>/SKILL.md` in the session's directory |
91
+ | `memory` | instructions `aviary.memory`; with `tools`, a `remember` tool when the provider writes (on the engine's endpoint only) |
92
+ | `ctx.emitUi` in a tool | the component lands in the turn's stream and message (see below) |
93
+ | `regenerate` | the session is reverted to before the last user message |
94
+
95
+ ## Tools over MCP
96
+
97
+ With `tools`, the engine mounts its own MCP endpoint at `POST <agent path>/opencode/mcp` and
98
+ registers it in every session (`mcp.add`) with a bearer token it mints: signed with `tools.secret`,
99
+ naming the turn's actor and the OpenCode server, expiring after `tools.ttlMs` (7 days; re-issued on a
100
+ kept session's turns once half-way through). Use the same `secret` in every process.
101
+
102
+ ```ts
103
+ AgentModule.forRoot({
104
+ engine: openCode({
105
+ host: MyOpenCodeHost,
106
+ // Where OpenCode reaches the app — from where OpenCode runs.
107
+ tools: { url: 'http://app.internal:3000/agent/opencode/mcp', secret: process.env.OPENCODE_TOOLS_SECRET },
108
+ }),
109
+ memory: { provider }, // a provider with `write` → OpenCode gets `remember`
110
+ ...
111
+ }),
112
+ ```
113
+
114
+ The endpoint serves turns, nothing else. A token alone runs nothing:
115
+
116
+ - a call runs only while the token's actor has a turn running on the session the call names (OpenCode
117
+ puts it in `_meta`), on the token's server;
118
+ - the agent's (and persona's) allow-list, `enabled`, the roles policy and `canUse` apply, on
119
+ `tools/list` and on `tools/call`; the kinds only the loop serves are never offered;
120
+ - an `action` runs only against an approval granted in that turn (a person's, or the approval policy
121
+ saying none is needed): one call per approval. OpenCode's `ask` rules put the approval card in
122
+ front of the call; the endpoint checks it again, so a caller that skips OpenCode's rules (a model
123
+ in code mode that read the endpoint's headers) is refused;
124
+ - `remember` is served here only — it is not added to the module's registry, so it is not on
125
+ `AgentMcpServerModule` or the `/tools` catalog.
126
+
127
+ The module's `guards` are not applied to the endpoint (its callers are OpenCode sessions, not the
128
+ app's users); a global guard of your own must let `opencode/mcp` through.
129
+
130
+ ## Several processes
131
+
132
+ Use `openCodeDurable()`, a cross-process sink, and a shared session store:
133
+ `sessions: keyValueOpenCodeSessionStore(redis)`.
134
+
135
+ ## Testing against a real OpenCode
136
+
137
+ ```sh
138
+ OPENCODE_LIVE_URL=http://127.0.0.1:4096 OPENCODE_LIVE_PASSWORD=… \
139
+ OPENCODE_LIVE_MODEL=opencode-go/longcat-2.5-preview-free OPENCODE_LIVE_DIR=/tmp/work \
140
+ pnpm vitest run packages/opencode/src/live
141
+ ```
142
+
143
+ The server (`opencode serve`, 2.x, `OPENCODE_SERVER_PASSWORD` set) needs a key for the model's
144
+ provider (`integration.connect.key`). Without `OPENCODE_LIVE_URL` the live specs are skipped.
145
+
146
+ See `docs/design/2026-10-06-opencode-engine.md` for what is not wired yet.