@neurocode-ai/protocol 1.18.9

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.
@@ -0,0 +1,379 @@
1
+ import { SessionMessage } from "@neurocode-ai/schema/session-message"
2
+ import { SessionInput } from "@neurocode-ai/schema/session-input"
3
+ import { PromptInput } from "@neurocode-ai/schema/prompt-input"
4
+ import { Session } from "@neurocode-ai/schema/session"
5
+ import { Project } from "@neurocode-ai/schema/project"
6
+ import { AbsolutePath, NonNegativeInt, PositiveInt, RelativePath, statics } from "@neurocode-ai/schema/schema"
7
+ import { Workspace } from "@neurocode-ai/schema/workspace"
8
+ import { Context, Effect, Encoding, Result, Schema, Struct } from "effect"
9
+ import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
10
+ import {
11
+ ConflictError,
12
+ InvalidCursorError,
13
+ InvalidRequestError,
14
+ MessageNotFoundError,
15
+ ServiceUnavailableError,
16
+ SessionNotFoundError,
17
+ UnknownError,
18
+ } from "../errors"
19
+ import { Agent } from "@neurocode-ai/schema/agent"
20
+ import { Model } from "@neurocode-ai/schema/model"
21
+ import { Location } from "@neurocode-ai/schema/location"
22
+ import { Revert } from "@neurocode-ai/schema/revert"
23
+ import { SessionEvent } from "@neurocode-ai/schema/session-event"
24
+
25
+ const SessionsQueryFields = {
26
+ workspace: Workspace.ID.pipe(Schema.optional),
27
+ limit: Schema.NumberFromString.pipe(Schema.decodeTo(PositiveInt), Schema.optional).annotate({
28
+ description: "Maximum number of sessions to return. Defaults to the newest 50 sessions.",
29
+ }),
30
+ order: Schema.optional(Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")])).annotate({
31
+ description: "Session order for the first page. Use desc for newest first or asc for oldest first.",
32
+ }),
33
+ search: Schema.optional(Schema.String),
34
+ }
35
+
36
+ const SessionsDirectoryQuery = Schema.Struct({
37
+ ...SessionsQueryFields,
38
+ directory: AbsolutePath,
39
+ })
40
+
41
+ const SessionsProjectQuery = Schema.Struct({
42
+ ...SessionsQueryFields,
43
+ project: Project.ID,
44
+ subpath: RelativePath.pipe(Schema.optional),
45
+ })
46
+
47
+ const SessionsAllQuery = Schema.Struct(SessionsQueryFields)
48
+
49
+ const withCursor = <Fields extends Schema.Struct.Fields>(schema: Schema.Struct<Fields>) =>
50
+ schema.mapFields((fields) => ({
51
+ ...Struct.omit(fields, ["limit"]),
52
+ anchor: Session.ListAnchor,
53
+ }))
54
+
55
+ const SessionsCursorInput = Schema.Union([
56
+ withCursor(SessionsDirectoryQuery),
57
+ withCursor(SessionsProjectQuery),
58
+ withCursor(SessionsAllQuery),
59
+ ])
60
+ const SessionsCursorJson = Schema.fromJsonString(SessionsCursorInput)
61
+ const encodeSessionsCursor = Schema.encodeSync(SessionsCursorJson)
62
+ const decodeSessionsCursor = Schema.decodeUnknownEffect(SessionsCursorJson)
63
+ const invalidCursor = "Invalid cursor" as const
64
+
65
+ export const SessionsCursor = Schema.String.pipe(
66
+ Schema.brand("SessionsCursor"),
67
+ statics((schema) => {
68
+ const make = schema.make.bind(schema)
69
+ return {
70
+ make: (input: typeof SessionsCursorInput.Type) => make(Encoding.encodeBase64Url(encodeSessionsCursor(input))),
71
+ parse: (input: string) =>
72
+ Effect.suspend(() => {
73
+ const result = Encoding.decodeBase64UrlString(input)
74
+ return Result.isFailure(result)
75
+ ? Effect.fail(invalidCursor)
76
+ : decodeSessionsCursor(result.success).pipe(Effect.mapError(() => invalidCursor))
77
+ }),
78
+ }
79
+ }),
80
+ )
81
+ export type SessionsCursor = typeof SessionsCursor.Type
82
+
83
+ const SessionActive = Schema.Struct({
84
+ type: Schema.Literal("running"),
85
+ }).annotate({ identifier: "SessionActive" })
86
+
87
+ const SessionHistoryLimit = PositiveInt.check(Schema.isLessThanOrEqualTo(100))
88
+
89
+ export const SessionHistoryQuery = Schema.Struct({
90
+ limit: Schema.NumberFromString.pipe(Schema.decodeTo(SessionHistoryLimit), Schema.optional),
91
+ after: Schema.NumberFromString.pipe(Schema.decodeTo(NonNegativeInt), Schema.optional),
92
+ })
93
+
94
+ const SessionsQueryCursor = SessionsCursor.annotate({
95
+ description: "Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response.",
96
+ })
97
+
98
+ export const SessionsQuery = Schema.Struct({
99
+ ...SessionsQueryFields,
100
+ directory: AbsolutePath.pipe(Schema.optional),
101
+ project: Project.ID.pipe(Schema.optional),
102
+ subpath: RelativePath.pipe(Schema.optional),
103
+ cursor: SessionsQueryCursor.pipe(Schema.optional),
104
+ }).annotate({ identifier: "SessionsQuery" })
105
+
106
+ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLocationMiddleware: Context.Key<I, S>) =>
107
+ HttpApiGroup.make("server.session")
108
+ .add(
109
+ HttpApiEndpoint.get("session.list", "/api/session", {
110
+ query: SessionsQuery,
111
+ success: Schema.Struct({
112
+ data: Schema.Array(Session.Info),
113
+ cursor: Schema.Struct({
114
+ previous: SessionsCursor.pipe(Schema.optional),
115
+ next: SessionsCursor.pipe(Schema.optional),
116
+ }),
117
+ }).annotate({ identifier: "SessionsResponse" }),
118
+ error: [InvalidCursorError, InvalidRequestError],
119
+ }).annotateMerge(
120
+ OpenApi.annotations({
121
+ identifier: "v2.session.list",
122
+ summary: "List sessions",
123
+ description:
124
+ "Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list.",
125
+ }),
126
+ ),
127
+ )
128
+ .add(
129
+ HttpApiEndpoint.post("session.create", "/api/session", {
130
+ payload: Schema.Struct({
131
+ id: Session.ID.pipe(Schema.optional),
132
+ agent: Agent.ID.pipe(Schema.optional),
133
+ model: Model.Ref.pipe(Schema.optional),
134
+ location: Location.Ref.pipe(Schema.optional),
135
+ }),
136
+ success: Schema.Struct({ data: Session.Info }),
137
+ }).annotateMerge(
138
+ OpenApi.annotations({
139
+ identifier: "v2.session.create",
140
+ summary: "Create session",
141
+ description: "Create a session at the requested location.",
142
+ }),
143
+ ),
144
+ )
145
+ .add(
146
+ HttpApiEndpoint.get("session.active", "/api/session/active", {
147
+ success: Schema.Struct({ data: Schema.Record(Session.ID, SessionActive) }),
148
+ }).annotateMerge(
149
+ OpenApi.annotations({
150
+ identifier: "v2.session.active",
151
+ summary: "List active sessions",
152
+ description:
153
+ "Retrieve foreground Session drains currently owned by this OpenCode process. Sessions absent from the result are inactive.",
154
+ }),
155
+ ),
156
+ )
157
+ .add(
158
+ HttpApiEndpoint.get("session.get", "/api/session/:sessionID", {
159
+ params: { sessionID: Session.ID },
160
+ success: Schema.Struct({ data: Session.Info }),
161
+ error: SessionNotFoundError,
162
+ })
163
+ .middleware(sessionLocationMiddleware)
164
+ .annotateMerge(
165
+ OpenApi.annotations({
166
+ identifier: "v2.session.get",
167
+ summary: "Get session",
168
+ description: "Retrieve a session by ID.",
169
+ }),
170
+ ),
171
+ )
172
+ .add(
173
+ HttpApiEndpoint.post("session.switchAgent", "/api/session/:sessionID/agent", {
174
+ params: { sessionID: Session.ID },
175
+ payload: Schema.Struct({ agent: Agent.ID }),
176
+ success: HttpApiSchema.NoContent,
177
+ error: SessionNotFoundError,
178
+ })
179
+ .middleware(sessionLocationMiddleware)
180
+ .annotateMerge(
181
+ OpenApi.annotations({
182
+ identifier: "v2.session.switchAgent",
183
+ summary: "Switch session agent",
184
+ description: "Switch the agent used by subsequent provider turns.",
185
+ }),
186
+ ),
187
+ )
188
+ .add(
189
+ HttpApiEndpoint.post("session.switchModel", "/api/session/:sessionID/model", {
190
+ params: { sessionID: Session.ID },
191
+ payload: Schema.Struct({ model: Model.Ref }),
192
+ success: HttpApiSchema.NoContent,
193
+ error: SessionNotFoundError,
194
+ })
195
+ .middleware(sessionLocationMiddleware)
196
+ .annotateMerge(
197
+ OpenApi.annotations({
198
+ identifier: "v2.session.switchModel",
199
+ summary: "Switch session model",
200
+ description: "Switch the model used by subsequent provider turns.",
201
+ }),
202
+ ),
203
+ )
204
+ .add(
205
+ HttpApiEndpoint.post("session.prompt", "/api/session/:sessionID/prompt", {
206
+ params: { sessionID: Session.ID },
207
+ payload: Schema.Struct({
208
+ id: SessionMessage.ID.pipe(Schema.optional),
209
+ prompt: PromptInput.Prompt,
210
+ delivery: SessionInput.Delivery.pipe(Schema.optional),
211
+ resume: Schema.Boolean.pipe(Schema.optional),
212
+ }),
213
+ success: Schema.Struct({ data: SessionInput.Admitted }),
214
+ error: [ConflictError, SessionNotFoundError],
215
+ })
216
+ .middleware(sessionLocationMiddleware)
217
+ .annotateMerge(
218
+ OpenApi.annotations({
219
+ identifier: "v2.session.prompt",
220
+ summary: "Send message",
221
+ description: "Durably admit one session input and schedule agent-loop execution unless resume is false.",
222
+ }),
223
+ ),
224
+ )
225
+ .add(
226
+ HttpApiEndpoint.post("session.compact", "/api/session/:sessionID/compact", {
227
+ params: { sessionID: Session.ID },
228
+ success: HttpApiSchema.NoContent,
229
+ error: [SessionNotFoundError, ServiceUnavailableError],
230
+ })
231
+ .middleware(sessionLocationMiddleware)
232
+ .annotateMerge(
233
+ OpenApi.annotations({
234
+ identifier: "v2.session.compact",
235
+ summary: "Compact session",
236
+ description: "Compact a session conversation.",
237
+ }),
238
+ ),
239
+ )
240
+ .add(
241
+ HttpApiEndpoint.post("session.wait", "/api/session/:sessionID/wait", {
242
+ params: { sessionID: Session.ID },
243
+ success: HttpApiSchema.NoContent,
244
+ error: [SessionNotFoundError, ServiceUnavailableError],
245
+ })
246
+ .middleware(sessionLocationMiddleware)
247
+ .annotateMerge(
248
+ OpenApi.annotations({
249
+ identifier: "v2.session.wait",
250
+ summary: "Wait for session",
251
+ description: "Wait for a session agent loop to become idle.",
252
+ }),
253
+ ),
254
+ )
255
+ .add(
256
+ HttpApiEndpoint.post("session.revert.stage", "/api/session/:sessionID/revert/stage", {
257
+ params: { sessionID: Session.ID },
258
+ payload: Schema.Struct({ messageID: SessionMessage.ID, files: Schema.Boolean.pipe(Schema.optional) }),
259
+ success: Schema.Struct({ data: Revert.State }),
260
+ error: [MessageNotFoundError, SessionNotFoundError, UnknownError],
261
+ })
262
+ .middleware(sessionLocationMiddleware)
263
+ .annotateMerge(
264
+ OpenApi.annotations({
265
+ identifier: "v2.session.revert.stage",
266
+ summary: "Stage session revert",
267
+ description: "Stage or move a reversible session boundary and optionally apply its file changes.",
268
+ }),
269
+ ),
270
+ )
271
+ .add(
272
+ HttpApiEndpoint.post("session.revert.clear", "/api/session/:sessionID/revert/clear", {
273
+ params: { sessionID: Session.ID },
274
+ success: HttpApiSchema.NoContent,
275
+ error: [SessionNotFoundError, UnknownError],
276
+ })
277
+ .middleware(sessionLocationMiddleware)
278
+ .annotateMerge(OpenApi.annotations({ identifier: "v2.session.revert.clear", summary: "Clear staged revert" })),
279
+ )
280
+ .add(
281
+ HttpApiEndpoint.post("session.revert.commit", "/api/session/:sessionID/revert/commit", {
282
+ params: { sessionID: Session.ID },
283
+ success: HttpApiSchema.NoContent,
284
+ error: SessionNotFoundError,
285
+ })
286
+ .middleware(sessionLocationMiddleware)
287
+ .annotateMerge(
288
+ OpenApi.annotations({ identifier: "v2.session.revert.commit", summary: "Commit staged revert" }),
289
+ ),
290
+ )
291
+ .add(
292
+ HttpApiEndpoint.get("session.context", "/api/session/:sessionID/context", {
293
+ params: { sessionID: Session.ID },
294
+ success: Schema.Struct({ data: Schema.Array(SessionMessage.Message) }),
295
+ error: [SessionNotFoundError, UnknownError],
296
+ })
297
+ .middleware(sessionLocationMiddleware)
298
+ .annotateMerge(
299
+ OpenApi.annotations({
300
+ identifier: "v2.session.context",
301
+ summary: "Get session context",
302
+ description: "Retrieve the active context messages for a session (all messages after the last compaction).",
303
+ }),
304
+ ),
305
+ )
306
+ .add(
307
+ HttpApiEndpoint.get("session.history", "/api/session/:sessionID/history", {
308
+ params: { sessionID: Session.ID },
309
+ query: SessionHistoryQuery,
310
+ success: Schema.Struct({
311
+ data: Schema.Array(SessionEvent.Durable),
312
+ hasMore: Schema.Boolean,
313
+ }).annotate({ identifier: "SessionHistory" }),
314
+ error: SessionNotFoundError,
315
+ })
316
+ .middleware(sessionLocationMiddleware)
317
+ .annotateMerge(
318
+ OpenApi.annotations({
319
+ identifier: "v2.session.history",
320
+ summary: "Get session history",
321
+ description:
322
+ "Read one finite page of public durable Session events after an exclusive aggregate sequence. Newly committed events may appear on later pages.",
323
+ }),
324
+ ),
325
+ )
326
+ .add(
327
+ HttpApiEndpoint.get("session.events", "/api/session/:sessionID/event", {
328
+ params: { sessionID: Session.ID },
329
+ query: {
330
+ after: Schema.NumberFromString.pipe(Schema.decodeTo(NonNegativeInt), Schema.optional),
331
+ },
332
+ success: HttpApiSchema.StreamSse({ data: SessionEvent.Durable }),
333
+ error: SessionNotFoundError,
334
+ })
335
+ .middleware(sessionLocationMiddleware)
336
+ .annotateMerge(
337
+ OpenApi.annotations({
338
+ identifier: "v2.session.events",
339
+ summary: "Subscribe to session events",
340
+ description: "Replay durable events after an aggregate sequence, then continue with new durable events.",
341
+ }),
342
+ ),
343
+ )
344
+ .add(
345
+ HttpApiEndpoint.post("session.interrupt", "/api/session/:sessionID/interrupt", {
346
+ params: { sessionID: Session.ID },
347
+ success: HttpApiSchema.NoContent,
348
+ error: SessionNotFoundError,
349
+ })
350
+ .middleware(sessionLocationMiddleware)
351
+ .annotateMerge(
352
+ OpenApi.annotations({
353
+ identifier: "v2.session.interrupt",
354
+ summary: "Interrupt session execution",
355
+ description: "Interrupt active execution owned by this OpenCode process. Idle interruption is a no-op.",
356
+ }),
357
+ ),
358
+ )
359
+ .add(
360
+ HttpApiEndpoint.get("session.message", "/api/session/:sessionID/message/:messageID", {
361
+ params: { sessionID: Session.ID, messageID: SessionMessage.ID },
362
+ success: Schema.Struct({ data: SessionMessage.Message }),
363
+ error: [SessionNotFoundError, MessageNotFoundError],
364
+ })
365
+ .middleware(sessionLocationMiddleware)
366
+ .annotateMerge(
367
+ OpenApi.annotations({
368
+ identifier: "v2.session.message",
369
+ summary: "Get session message",
370
+ description: "Retrieve one projected message owned by the Session.",
371
+ }),
372
+ ),
373
+ )
374
+ .annotateMerge(
375
+ OpenApi.annotations({
376
+ title: "sessions",
377
+ description: "Experimental session routes.",
378
+ }),
379
+ )
@@ -0,0 +1,27 @@
1
+ import { Skill } from "@neurocode-ai/schema/skill"
2
+ import { Location } from "@neurocode-ai/schema/location"
3
+ import { Schema } from "effect"
4
+ import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
5
+ import { LocationQuery, locationQueryOpenApi } from "./location"
6
+
7
+ export const SkillGroup = HttpApiGroup.make("server.skill")
8
+ .add(
9
+ HttpApiEndpoint.get("skill.list", "/api/skill", {
10
+ query: LocationQuery,
11
+ success: Location.response(Schema.Array(Skill.Info)),
12
+ })
13
+ .annotateMerge(locationQueryOpenApi)
14
+ .annotateMerge(
15
+ OpenApi.annotations({
16
+ identifier: "v2.skill.list",
17
+ summary: "List skills",
18
+ description: "Retrieve currently registered skills.",
19
+ }),
20
+ ),
21
+ )
22
+ .annotateMerge(
23
+ OpenApi.annotations({
24
+ title: "skills",
25
+ description: "Experimental skill routes.",
26
+ }),
27
+ )
@@ -0,0 +1,6 @@
1
+ import { HttpApiMiddleware } from "effect/unstable/httpapi"
2
+ import { UnauthorizedError } from "../errors"
3
+
4
+ export class Authorization extends HttpApiMiddleware.Service<Authorization>()("@neurocode/HttpApiAuthorization", {
5
+ error: UnauthorizedError,
6
+ }) {}
@@ -0,0 +1,7 @@
1
+ import { HttpApiMiddleware } from "effect/unstable/httpapi"
2
+ import { InvalidRequestError } from "../errors"
3
+
4
+ export class SchemaErrorMiddleware extends HttpApiMiddleware.Service<SchemaErrorMiddleware>()(
5
+ "@neurocode/HttpApiSchemaError",
6
+ { error: InvalidRequestError },
7
+ ) {}
package/sst-env.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /* This file is auto-generated by SST. Do not edit. */
2
+ /* tslint:disable */
3
+ /* eslint-disable */
4
+ /* deno-fmt-ignore-file */
5
+ /* biome-ignore-all lint: auto-generated */
6
+
7
+ /// <reference path="../../sst-env.d.ts" />
8
+
9
+ import "sst"
10
+ export {}
@@ -0,0 +1,26 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { Effect, Schema } from "effect"
3
+ import { SessionHistoryQuery, SessionsCursor } from "../src/groups/session"
4
+ import { Session } from "@neurocode-ai/schema/session"
5
+
6
+ describe("SessionsCursor", () => {
7
+ test("round trips without Node globals", async () => {
8
+ const input = {
9
+ workspace: undefined,
10
+ search: "protocol",
11
+ order: "desc" as const,
12
+ anchor: { id: Session.ID.make("ses_test"), time: 1, direction: "next" as const },
13
+ }
14
+ const cursor = SessionsCursor.make(input)
15
+
16
+ expect(await Effect.runPromise(SessionsCursor.parse(cursor))).toEqual(input)
17
+ })
18
+ })
19
+
20
+ describe("SessionHistoryQuery", () => {
21
+ test("decodes numeric paging inputs", async () => {
22
+ const query = await Effect.runPromise(Schema.decodeUnknownEffect(SessionHistoryQuery)({ after: "3", limit: "10" }))
23
+
24
+ expect(query).toEqual({ after: 3, limit: 10 })
25
+ })
26
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "extends": "@tsconfig/bun/tsconfig.json",
4
+ "compilerOptions": {
5
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
6
+ "noUncheckedIndexedAccess": false
7
+ }
8
+ }