@dudousxd/nestjs-agent-opencode 0.5.0 → 1.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/README.md CHANGED
@@ -120,6 +120,94 @@ of settling it as an empty success. It needs the client's `message.list`. `openC
120
120
  same steps in memory (single replica). Several processes need a cross-process sink and a persistent
121
121
  `OpenCodeSessionStore`.
122
122
 
123
+ ### Limits, stops and lifecycle hooks
124
+
125
+ Everything here is optional; without these options a turn runs exactly as before. They work with
126
+ `openCode()` too.
127
+
128
+ ```ts
129
+ openCodeDurable({
130
+ host: MyOpenCodeHost,
131
+ // Per engine, or `(input) => limits` per turn.
132
+ limits: { steps: 40, compactions: 2, wallClockMs: 10 * 60_000 },
133
+ onTurnStart: ({ turnId, threadId, sessionId, resumed }) => turnState.open(turnId),
134
+ onTurnEvent: ({ turnId, type, sessionId }) => turnState.saw(turnId, type),
135
+ onStop: ({ turnId, reason, counts }) => credentials.revoke(turnId),
136
+ onTurnEnd: ({ turnId, status, stop }) => turnState.close(turnId, status, stop?.reason),
137
+ });
138
+
139
+ // A stop the app starts (a revoked credential, a budget it keeps itself):
140
+ await app.get(OpenCodeTurns).stopTurn(runId, 'budget');
141
+ // → 'aborted' | 'settling' | 'pending' | 'ignored' | 'unconfirmed'
142
+ ```
143
+
144
+ | Option | Type | What it does |
145
+ | --- | --- | --- |
146
+ | `limits.steps` | `number` | Model rounds (`session.step.started`) the turn may run; the next one stops it. |
147
+ | `limits.compactions` | `number` | Compactions (`session.compaction.started`) the turn may run; the next one stops it. |
148
+ | `limits.wallClockMs` | `number` | Stops the turn this long after its `begin`, time parked on a person included. |
149
+ | `onTurnStart` | `({ turnId, threadId, sessionId, resumed }) => void \| Promise<void>` | This process starts executing the turn; `resumed: true` when it took the run over from another (a crash, a run resumed elsewhere after a person answered). |
150
+ | `onTurnEvent` | `({ turnId, type, sessionId }) => void` | One OpenCode event of the turn's own session: its type, never its payload. |
151
+ | `onStop` | `({ turnId, reason, counts }) => void \| Promise<void>` | A stop landed. `counts` is `{ steps, compactions, elapsedMs }`. |
152
+ | `onTurnEnd` | `({ turnId, status, stop? }) => void \| Promise<void>` | This process is done with the turn. `status` is `'succeeded' \| 'failed' \| 'interrupted' \| 'released'`. |
153
+ | `hookTimeoutMs` | `number` | How long a step waits for `onTurnStart`. Default 5 seconds; must be positive. |
154
+
155
+ Limits count what a turn may do: a limit of N allows N steps (or compactions), and the next one
156
+ stops the turn. `turnId` is the run id. Only OpenCode's events for the turn's own session count
157
+ (another thread's session or a subagent's do not).
158
+
159
+ **How a stop ends the turn.** A stop aborts the session (`session.interrupt`), so OpenCode starts no
160
+ new round. The turn ends `failed` with `outcome.stop = { reason, counts }`, which the host's
161
+ `beforeSettle` and `onSettled` see on the `OpenCodeRunResult`. The run row and the stream carry the
162
+ error code `opencode_stopped`, and the durable workflow returns `{ outcome: 'failed', stop: reason }`.
163
+ `reason` is `'steps' | 'compactions' | 'wall-clock'`, or what the app passed to `stopTurn`. Return
164
+ `{ error }` from `beforeSettle` to word the message the person reads. When the answer was already
165
+ settling as the stop landed, the answer is kept: the turn ends `succeeded`, with `stop` still set on
166
+ the outcome, `onTurnEnd` and the workflow result, and `onStop` still called.
167
+
168
+ **What a stop aborts.** A thread's session is reused by its next turn. So, immediately before the
169
+ abort, the engine checks that the turn is still running here, still owns its session, and is still
170
+ the run the thread's store holds. If any check fails, the stop is `'ignored'`. If the store cannot be
171
+ read, nothing is aborted and the stop is `'unconfirmed'` (ask again). A stop for a turn that ended
172
+ does nothing.
173
+
174
+ **A stop while the turn waits on a person** returns `'pending'`. The ask it waits on is refused,
175
+ whatever the person answers (an approval becomes a rejection, a form is skipped), so no action runs.
176
+ The stop then lands as soon as the turn observes OpenCode again. A pending stop on a turn that is
177
+ then cancelled while parked (released as `interrupted`, below) is dropped with it: no `onStop`
178
+ fires, and `onTurnEnd` carries no `stop`. If the OpenCode server restarted
179
+ while the turn waited, no new session is opened or prompted: the turn ends stopped. To end a parked run at once, cancel
180
+ it.
181
+
182
+ **Where the hooks run.** The hooks are called in the process that executes the turn's steps. With
183
+ `openCodeDurable()` any worker may claim a run, so that is the worker that runs them, not the one
184
+ that admitted the question. They can also be set on the host (both are called, the options' first).
185
+ Their errors are logged and never fail the turn. Only `onTurnStart` is waited for, at most
186
+ `hookTimeoutMs`; the others are called and not waited for.
187
+
188
+ **`onTurnEnd` is paired with `onTurnStart`.** Every process that gets `onTurnStart` for a turn gets
189
+ exactly one `onTurnEnd` for it, and its per-turn state goes with it:
190
+
191
+ - The turn settled there: `succeeded`, `failed` or `interrupted`.
192
+ - It was cancelled from another process while parked there: `interrupted`. The session's execution
193
+ was interrupted under the parked turn, so the store is read again: right away, then at about
194
+ 0.25, 1.25, 4.25, 14.25 and 44.25 seconds (one read at a time, at most one every 250 ms). The turn
195
+ is let go when the store shows the ask settled, when the store cannot tell (no `toolCallApproval`,
196
+ a failed read, the ask not on file) after the first recheck, or when the rechecks run out.
197
+ - Another worker went on with it after a person answered: `released`. This fires on the next round
198
+ (`session.step.started`), or when an event of the session finds the parked ask decided in the
199
+ store by someone else.
200
+ - The process shut down while executing it: `released`.
201
+
202
+ While a turn is parked here, OpenCode may keep running the step's other tool calls. Their events
203
+ are streamed and recorded as usual, and they do not count as the turn moving elsewhere. If the
204
+ session's execution ends some other way (succeeded or failed) while the ask is still open, the turn
205
+ stays here for its answer, and a stop asked meanwhile returns `'pending'`.
206
+
207
+ A process that never executed the turn (the API that admitted or cancelled it) gets no hooks.
208
+ `stopTurn` acts only in the process executing the turn; anywhere else, and after `onTurnEnd`, it
209
+ returns `'ignored'`.
210
+
123
211
  ## What the session gets from the module
124
212
 
125
213
  | Option | In the session |
@@ -188,7 +276,7 @@ The endpoint serves turns, nothing else. A token alone runs nothing:
188
276
  - `remember` is served here only — it is not added to the module's registry, so it is not on
189
277
  `AgentMcpServerModule` or the `/tools` catalog.
190
278
 
191
- The module's `guards` are not applied to the endpoint (its callers are OpenCode sessions, not the
279
+ The module's `decorators` are not applied to the endpoint (its callers are OpenCode sessions, not the
192
280
  app's users); a global guard of your own must let `opencode/mcp` through.
193
281
 
194
282
  ## Several processes