clanka 0.2.71 → 0.3.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 (57) hide show
  1. package/dist/Acp.d.ts +88 -0
  2. package/dist/Acp.d.ts.map +1 -0
  3. package/dist/Acp.js +387 -0
  4. package/dist/Acp.js.map +1 -0
  5. package/dist/Acp.test.d.ts +2 -0
  6. package/dist/Acp.test.d.ts.map +1 -0
  7. package/dist/Acp.test.js +179 -0
  8. package/dist/Acp.test.js.map +1 -0
  9. package/dist/AgentExecutor.d.ts +36 -36
  10. package/dist/AgentExecutor.d.ts.map +1 -1
  11. package/dist/AgentExecutor.js +5 -5
  12. package/dist/AgentExecutor.js.map +1 -1
  13. package/dist/AgentOutput.js +1 -1
  14. package/dist/AgentOutput.js.map +1 -1
  15. package/dist/ChunkRepo.js +1 -1
  16. package/dist/ChunkRepo.js.map +1 -1
  17. package/dist/CodeChunker.js +1 -1
  18. package/dist/CodeChunker.js.map +1 -1
  19. package/dist/CodexAuth.js +1 -1
  20. package/dist/CodexAuth.js.map +1 -1
  21. package/dist/CopilotAuth.js +1 -1
  22. package/dist/CopilotAuth.js.map +1 -1
  23. package/dist/DeviceCodeHandler.d.ts +2 -1
  24. package/dist/DeviceCodeHandler.d.ts.map +1 -1
  25. package/dist/DeviceCodeHandler.js +4 -0
  26. package/dist/DeviceCodeHandler.js.map +1 -1
  27. package/dist/ExaSearch.js +1 -1
  28. package/dist/ExaSearch.js.map +1 -1
  29. package/dist/McpClient.js +1 -1
  30. package/dist/McpClient.js.map +1 -1
  31. package/dist/SemanticSearch.d.ts +2 -2
  32. package/dist/SemanticSearch.d.ts.map +1 -1
  33. package/dist/SemanticSearch.js.map +1 -1
  34. package/dist/TypeBuilder.js +1 -1
  35. package/dist/TypeBuilder.js.map +1 -1
  36. package/dist/cli.js +40 -29
  37. package/dist/cli.js.map +1 -1
  38. package/dist/index.d.ts +4 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +4 -0
  41. package/dist/index.js.map +1 -1
  42. package/package.json +21 -21
  43. package/src/Acp.test.ts +288 -0
  44. package/src/Acp.ts +596 -0
  45. package/src/AgentExecutor.ts +18 -24
  46. package/src/AgentOutput.ts +1 -1
  47. package/src/ChunkRepo.ts +1 -1
  48. package/src/CodeChunker.ts +1 -1
  49. package/src/CodexAuth.ts +1 -1
  50. package/src/CopilotAuth.ts +1 -1
  51. package/src/DeviceCodeHandler.ts +8 -1
  52. package/src/ExaSearch.ts +1 -1
  53. package/src/McpClient.ts +1 -1
  54. package/src/SemanticSearch.ts +2 -2
  55. package/src/TypeBuilder.ts +1 -1
  56. package/src/cli.ts +100 -46
  57. package/src/index.ts +5 -0
package/src/Acp.ts ADDED
@@ -0,0 +1,596 @@
1
+ /** @effect-diagnostics schemaNumber:off */
2
+ /**
3
+ * Agent Client Protocol (ACP) server for clanka.
4
+ *
5
+ * Speaks JSON-RPC 2.0 over stdio so ACP clients (Zed, Multica, ...) can drive
6
+ * an Agent. Sessions are persisted to the KeyValueStore so `session/load`
7
+ * works across processes.
8
+ *
9
+ * @since 1.0.0
10
+ */
11
+ import { randomUUID } from "node:crypto"
12
+ import * as Cause from "effect/Cause"
13
+ import * as Effect from "effect/Effect"
14
+ import * as Exit from "effect/Exit"
15
+ import * as Fiber from "effect/Fiber"
16
+ import type * as Layer from "effect/Layer"
17
+ import * as MutableRef from "effect/MutableRef"
18
+ import * as Option from "effect/Option"
19
+ import type * as PlatformError from "effect/PlatformError"
20
+ import * as Queue from "effect/Queue"
21
+ import * as Schema from "effect/Schema"
22
+ import * as Scope from "effect/Scope"
23
+ import * as Stdio from "effect/Stdio"
24
+ import * as Stream from "effect/Stream"
25
+ import type * as LanguageModel from "effect/unstable/ai/LanguageModel"
26
+ import type * as Model from "effect/unstable/ai/Model"
27
+ import * as Prompt from "effect/unstable/ai/Prompt"
28
+ import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore"
29
+ import type * as Agent from "./Agent.ts"
30
+ import type * as AgentOutput from "./AgentOutput.ts"
31
+
32
+ /**
33
+ * @since 1.0.0
34
+ * @category Models
35
+ */
36
+ export type ModelServices =
37
+ | LanguageModel.LanguageModel
38
+ | Model.ProviderName
39
+ | Model.ModelName
40
+ | Agent.SubagentModel
41
+
42
+ /**
43
+ * @since 1.0.0
44
+ * @category Models
45
+ */
46
+ export interface Options<RAgent, RModel> {
47
+ readonly version: string
48
+ /**
49
+ * Model id used when a client does not select one. Model ids are opaque to
50
+ * the server and resolved with `makeModel`.
51
+ */
52
+ readonly defaultModel: string
53
+ /**
54
+ * Write one JSON-RPC message to the client.
55
+ */
56
+ readonly send: (message: object) => Effect.Effect<void>
57
+ /**
58
+ * Create the Agent backing a session rooted at `cwd`.
59
+ */
60
+ readonly makeAgent: (
61
+ cwd: string,
62
+ ) => Effect.Effect<Agent.Agent, never, Scope.Scope | RAgent>
63
+ /**
64
+ * Resolve a model id to the services an Agent needs. `None` rejects the id.
65
+ */
66
+ readonly makeModel: (
67
+ modelId: string,
68
+ ) => Option.Option<Layer.Layer<ModelServices, never, RModel>>
69
+ }
70
+
71
+ /**
72
+ * @since 1.0.0
73
+ * @category Models
74
+ */
75
+ export interface Server<R = never> {
76
+ /**
77
+ * Handle one line of JSON-RPC input. Completes once the message has been
78
+ * fully processed, including any responses sent.
79
+ */
80
+ readonly handle: (line: string) => Effect.Effect<void, never, R>
81
+ }
82
+
83
+ /**
84
+ * @since 1.0.0
85
+ * @category Models
86
+ */
87
+ export class SessionRecord extends Schema.Class<SessionRecord>(
88
+ "clanka/Acp/SessionRecord",
89
+ )({
90
+ cwd: Schema.String,
91
+ model: Schema.String,
92
+ history: Prompt.Prompt,
93
+ }) {}
94
+
95
+ /**
96
+ * @since 1.0.0
97
+ * @category Errors
98
+ */
99
+ export class RpcError extends Schema.TaggedError<RpcError>()("AcpRpcError", {
100
+ code: Schema.Number,
101
+ message: Schema.String,
102
+ }) {}
103
+
104
+ const RequestId = Schema.Union([Schema.Number, Schema.String])
105
+
106
+ const IncomingMessage = Schema.Struct({
107
+ id: Schema.optionalKey(RequestId),
108
+ method: Schema.optionalKey(Schema.String),
109
+ params: Schema.optionalKey(Schema.Unknown),
110
+ })
111
+
112
+ const decodeIncoming = Schema.decodeEffect(
113
+ Schema.fromJsonString(IncomingMessage),
114
+ )
115
+
116
+ const NewSessionParams = Schema.Struct({
117
+ cwd: Schema.String,
118
+ model: Schema.optionalKey(Schema.String),
119
+ })
120
+
121
+ const LoadSessionParams = Schema.Struct({
122
+ sessionId: Schema.String,
123
+ cwd: Schema.optionalKey(Schema.String),
124
+ model: Schema.optionalKey(Schema.String),
125
+ })
126
+
127
+ const SessionIdParams = Schema.Struct({
128
+ sessionId: Schema.String,
129
+ })
130
+
131
+ const SetModelParams = Schema.Struct({
132
+ sessionId: Schema.String,
133
+ modelId: Schema.String,
134
+ })
135
+
136
+ const TextBlock = Schema.Struct({
137
+ type: Schema.Literal("text"),
138
+ text: Schema.String,
139
+ })
140
+
141
+ const ResourceLinkBlock = Schema.Struct({
142
+ type: Schema.Literal("resource_link"),
143
+ uri: Schema.String,
144
+ name: Schema.optionalKey(Schema.String),
145
+ })
146
+
147
+ const ResourceBlock = Schema.Struct({
148
+ type: Schema.Literal("resource"),
149
+ resource: Schema.Struct({
150
+ uri: Schema.String,
151
+ text: Schema.optionalKey(Schema.String),
152
+ }),
153
+ })
154
+
155
+ const ContentBlock = Schema.Union([TextBlock, ResourceLinkBlock, ResourceBlock])
156
+
157
+ const PromptParams = Schema.Struct({
158
+ sessionId: Schema.String,
159
+ prompt: Schema.Array(ContentBlock),
160
+ })
161
+
162
+ const decodeParams = <S extends Schema.Top>(schema: S, params: unknown) =>
163
+ Schema.decodeUnknownEffect(schema)(params).pipe(
164
+ Effect.mapError(
165
+ (error) => new RpcError({ code: -32602, message: error.message }),
166
+ ),
167
+ )
168
+
169
+ const sessionNotFound = (sessionId: string) =>
170
+ new RpcError({ code: -32602, message: `Session not found: ${sessionId}` })
171
+
172
+ const toRpcError = (cause: Cause.Cause<unknown>) => {
173
+ const error = Cause.squash(cause)
174
+ return error instanceof RpcError
175
+ ? error
176
+ : new RpcError({ code: -32603, message: Cause.pretty(cause) })
177
+ }
178
+
179
+ const renderPrompt = (
180
+ blocks: ReadonlyArray<typeof ContentBlock.Type>,
181
+ ): string =>
182
+ blocks
183
+ .map((block) => {
184
+ switch (block.type) {
185
+ case "text":
186
+ return block.text
187
+ case "resource_link":
188
+ return `[${block.name ?? block.uri}](${block.uri})`
189
+ case "resource":
190
+ return block.resource.text === undefined
191
+ ? `[${block.resource.uri}](${block.resource.uri})`
192
+ : `<resource uri="${block.resource.uri}">\n${block.resource.text}\n</resource>`
193
+ }
194
+ })
195
+ .join("\n\n")
196
+
197
+ const textContent = (text: string) => ({ type: "text", text })
198
+
199
+ const historyUpdates = (history: Prompt.Prompt) => {
200
+ const updates: Array<object> = []
201
+ for (const message of history.content) {
202
+ if (message.role !== "user" && message.role !== "assistant") continue
203
+ const sessionUpdate =
204
+ message.role === "user" ? "user_message_chunk" : "agent_message_chunk"
205
+ for (const part of message.content) {
206
+ if (part.type === "text") {
207
+ updates.push({ sessionUpdate, content: textContent(part.text) })
208
+ }
209
+ }
210
+ }
211
+ return updates
212
+ }
213
+
214
+ interface Session {
215
+ readonly id: string
216
+ readonly cwd: string
217
+ model: string
218
+ readonly agent: Agent.Agent
219
+ running: Fiber.Fiber<void, unknown> | undefined
220
+ toolCalls: number
221
+ }
222
+
223
+ /**
224
+ * @since 1.0.0
225
+ * @category Constructors
226
+ */
227
+ export const make = Effect.fnUntraced(function* <RAgent, RModel>(
228
+ options: Options<RAgent, RModel>,
229
+ ): Effect.fn.Return<
230
+ Server<RAgent | RModel>,
231
+ never,
232
+ KeyValueStore.KeyValueStore | Scope.Scope
233
+ > {
234
+ const kvs = yield* KeyValueStore.KeyValueStore
235
+ const store = KeyValueStore.toSchemaStore(
236
+ KeyValueStore.prefix(kvs, "session-"),
237
+ SessionRecord,
238
+ )
239
+ const scope = yield* Effect.scope
240
+ const sessions = new Map<string, Session>()
241
+
242
+ const notify = (method: string, params: object) =>
243
+ options.send({ jsonrpc: "2.0", method, params })
244
+
245
+ const update = (sessionId: string, update: object) =>
246
+ notify("session/update", { sessionId, update })
247
+
248
+ const getSession = (sessionId: string) =>
249
+ Effect.fromOption(Option.fromNullishOr(sessions.get(sessionId)), () =>
250
+ sessionNotFound(sessionId),
251
+ )
252
+
253
+ const requireModel = (modelId: string) =>
254
+ Effect.fromOption(
255
+ options.makeModel(modelId),
256
+ () =>
257
+ new RpcError({ code: -32602, message: `Unknown model: ${modelId}` }),
258
+ )
259
+
260
+ const modelsInfo = (session: Session) => ({
261
+ availableModels: [...new Set([session.model, options.defaultModel])].map(
262
+ (modelId) => ({ modelId, name: modelId }),
263
+ ),
264
+ currentModelId: session.model,
265
+ })
266
+
267
+ const persist = (session: Session) =>
268
+ store
269
+ .set(
270
+ session.id,
271
+ new SessionRecord({
272
+ cwd: session.cwd,
273
+ model: session.model,
274
+ history: session.agent.history.current,
275
+ }),
276
+ )
277
+ .pipe(
278
+ Effect.catch((error) =>
279
+ Effect.logWarning("Failed to persist session", error),
280
+ ),
281
+ )
282
+
283
+ const openSession = Effect.fnUntraced(function* (
284
+ id: string,
285
+ record: SessionRecord,
286
+ ) {
287
+ const agent = yield* Scope.provide(options.makeAgent(record.cwd), scope)
288
+ MutableRef.set(agent.history, record.history)
289
+ const session: Session = {
290
+ id,
291
+ cwd: record.cwd,
292
+ model: record.model,
293
+ agent,
294
+ running: undefined,
295
+ toolCalls: 0,
296
+ }
297
+ sessions.set(id, session)
298
+ return session
299
+ })
300
+
301
+ const replayHistory = (session: Session) =>
302
+ Effect.forEach(
303
+ historyUpdates(session.agent.history.current),
304
+ (sessionUpdate) => update(session.id, sessionUpdate),
305
+ { discard: true },
306
+ )
307
+
308
+ const runTurn = Effect.fnUntraced(function* (
309
+ session: Session,
310
+ prompt: string,
311
+ ) {
312
+ const stream = yield* session.agent.send({ prompt })
313
+ let script = ""
314
+ let toolCallId = ""
315
+
316
+ const emit = (part: AgentOutput.Output): Effect.Effect<void> => {
317
+ switch (part._tag) {
318
+ case "ReasoningDelta":
319
+ return update(session.id, {
320
+ sessionUpdate: "agent_thought_chunk",
321
+ content: textContent(part.delta),
322
+ })
323
+ case "ScriptStart":
324
+ script = ""
325
+ return Effect.void
326
+ case "ScriptDelta":
327
+ script += part.delta
328
+ return Effect.void
329
+ case "ScriptEnd":
330
+ toolCallId = `execute-${++session.toolCalls}`
331
+ return update(session.id, {
332
+ sessionUpdate: "tool_call",
333
+ toolCallId,
334
+ title: "execute",
335
+ kind: "execute",
336
+ status: "in_progress",
337
+ rawInput: { script },
338
+ content: [{ type: "content", content: textContent(script) }],
339
+ })
340
+ case "ScriptOutput":
341
+ return update(session.id, {
342
+ sessionUpdate: "tool_call_update",
343
+ toolCallId,
344
+ status: "completed",
345
+ rawOutput: { output: part.output },
346
+ content: [{ type: "content", content: textContent(part.output) }],
347
+ })
348
+ case "Usage":
349
+ return update(session.id, {
350
+ sessionUpdate: "usage_update",
351
+ usage: {
352
+ inputTokens: part.inputTokens,
353
+ outputTokens: part.outputTokens,
354
+ },
355
+ })
356
+ case "ErrorRetry":
357
+ return update(session.id, {
358
+ sessionUpdate: "agent_thought_chunk",
359
+ content: textContent(
360
+ `Retrying after error: ${part.error.message}\n`,
361
+ ),
362
+ })
363
+ case "SubagentStart":
364
+ return update(session.id, {
365
+ sessionUpdate: "tool_call",
366
+ toolCallId: `subagent-${part.id}`,
367
+ title: "subagent",
368
+ kind: "think",
369
+ status: "in_progress",
370
+ rawInput: { prompt: part.prompt },
371
+ })
372
+ case "SubagentComplete":
373
+ return update(session.id, {
374
+ sessionUpdate: "tool_call_update",
375
+ toolCallId: `subagent-${part.id}`,
376
+ status: "completed",
377
+ rawOutput: { summary: part.summary },
378
+ content: [{ type: "content", content: textContent(part.summary) }],
379
+ })
380
+ default:
381
+ return Effect.void
382
+ }
383
+ }
384
+
385
+ yield* stream.pipe(
386
+ Stream.runForEach(emit),
387
+ Effect.catchTag("AgentFinished", (finished) =>
388
+ update(session.id, {
389
+ sessionUpdate: "agent_message_chunk",
390
+ content: textContent(finished.summary),
391
+ }),
392
+ ),
393
+ )
394
+ })
395
+
396
+ const initialize = Effect.succeed({
397
+ protocolVersion: 1,
398
+ agentCapabilities: {
399
+ loadSession: true,
400
+ promptCapabilities: { image: false, audio: false, embeddedContext: true },
401
+ mcpCapabilities: { http: false, sse: false },
402
+ },
403
+ authMethods: [],
404
+ agentInfo: { name: "clanka", version: options.version },
405
+ })
406
+
407
+ const newSession = Effect.fnUntraced(function* (params: unknown) {
408
+ const { cwd, model } = yield* decodeParams(NewSessionParams, params)
409
+ const modelId = model ?? options.defaultModel
410
+ yield* requireModel(modelId)
411
+ const session = yield* openSession(
412
+ randomUUID(),
413
+ new SessionRecord({ cwd, model: modelId, history: Prompt.empty }),
414
+ )
415
+ yield* persist(session)
416
+ return { sessionId: session.id, models: modelsInfo(session) }
417
+ })
418
+
419
+ const loadSession = Effect.fnUntraced(function* (
420
+ params: unknown,
421
+ replay: boolean,
422
+ ) {
423
+ const { sessionId, cwd, model } = yield* decodeParams(
424
+ LoadSessionParams,
425
+ params,
426
+ )
427
+ const existing = sessions.get(sessionId)
428
+ if (existing !== undefined) {
429
+ return { models: modelsInfo(existing) }
430
+ }
431
+ const record = yield* store
432
+ .get(sessionId)
433
+ .pipe(
434
+ Effect.mapError(
435
+ (error) => new RpcError({ code: -32603, message: error.message }),
436
+ ),
437
+ )
438
+ if (Option.isNone(record)) {
439
+ return yield* sessionNotFound(sessionId)
440
+ }
441
+ const modelId = model ?? record.value.model
442
+ yield* requireModel(modelId)
443
+ const session = yield* openSession(
444
+ sessionId,
445
+ new SessionRecord({
446
+ cwd: cwd ?? record.value.cwd,
447
+ model: modelId,
448
+ history: record.value.history,
449
+ }),
450
+ )
451
+ if (replay) {
452
+ yield* replayHistory(session)
453
+ }
454
+ return { models: modelsInfo(session) }
455
+ })
456
+
457
+ const setModel = Effect.fnUntraced(function* (params: unknown) {
458
+ const { sessionId, modelId } = yield* decodeParams(SetModelParams, params)
459
+ const session = yield* getSession(sessionId)
460
+ yield* requireModel(modelId)
461
+ session.model = modelId
462
+ yield* persist(session)
463
+ return {}
464
+ })
465
+
466
+ const prompt = Effect.fnUntraced(function* (params: unknown) {
467
+ const { sessionId, prompt } = yield* decodeParams(PromptParams, params)
468
+ const session = yield* getSession(sessionId)
469
+ if (session.running !== undefined) {
470
+ return yield* new RpcError({
471
+ code: -32000,
472
+ message: `Session ${sessionId} already has a prompt in progress`,
473
+ })
474
+ }
475
+ const model = yield* requireModel(session.model)
476
+ session.running = yield* runTurn(session, renderPrompt(prompt)).pipe(
477
+ Effect.provide(model),
478
+ Effect.scoped,
479
+ Effect.forkChild,
480
+ )
481
+ const exit = yield* Effect.exit(Fiber.join(session.running))
482
+ session.running = undefined
483
+ yield* persist(session)
484
+ if (Exit.isSuccess(exit)) return { stopReason: "end_turn" }
485
+ if (Cause.hasInterruptsOnly(exit.cause)) return { stopReason: "cancelled" }
486
+ return yield* toRpcError(exit.cause)
487
+ })
488
+
489
+ const cancel = Effect.fnUntraced(function* (params: unknown) {
490
+ const { sessionId } = yield* decodeParams(SessionIdParams, params)
491
+ const running = sessions.get(sessionId)?.running
492
+ if (running !== undefined) {
493
+ yield* Fiber.interrupt(running)
494
+ }
495
+ })
496
+
497
+ const dispatch = (
498
+ method: string,
499
+ params: unknown,
500
+ ): Effect.Effect<object, RpcError, RAgent | RModel> => {
501
+ switch (method) {
502
+ case "initialize":
503
+ return initialize
504
+ case "authenticate":
505
+ case "session/set_mode":
506
+ case "session/set_config_option":
507
+ return Effect.succeed({})
508
+ case "session/new":
509
+ return newSession(params)
510
+ case "session/load":
511
+ return loadSession(params, true)
512
+ case "session/resume":
513
+ return loadSession(params, false)
514
+ case "session/set_model":
515
+ return setModel(params)
516
+ case "session/prompt":
517
+ return prompt(params)
518
+ default:
519
+ return Effect.fail(
520
+ new RpcError({
521
+ code: -32601,
522
+ message: `Method not found: ${method}`,
523
+ }),
524
+ )
525
+ }
526
+ }
527
+
528
+ const handle = Effect.fnUntraced(
529
+ function* (line: string) {
530
+ if (line.trim() === "") return
531
+ const message = yield* decodeIncoming(line)
532
+ // Responses to requests are ignored: the server never sends requests
533
+ if (message.method === undefined) return
534
+ if (message.id === undefined) {
535
+ if (message.method === "session/cancel") {
536
+ yield* cancel(message.params)
537
+ }
538
+ return
539
+ }
540
+ const id = message.id
541
+ yield* dispatch(message.method, message.params).pipe(
542
+ Effect.matchCauseEffect({
543
+ onSuccess: (result) => options.send({ jsonrpc: "2.0", id, result }),
544
+ onFailure: (cause) => {
545
+ const error = toRpcError(cause)
546
+ return options.send({
547
+ jsonrpc: "2.0",
548
+ id,
549
+ error: { code: error.code, message: error.message },
550
+ })
551
+ },
552
+ }),
553
+ )
554
+ },
555
+ Effect.catch((error) =>
556
+ Effect.logWarning("Ignoring invalid JSON-RPC message", error),
557
+ ),
558
+ )
559
+
560
+ return { handle }
561
+ })
562
+
563
+ /**
564
+ * Run the ACP server over the process stdio.
565
+ *
566
+ * @since 1.0.0
567
+ * @category Constructors
568
+ */
569
+ export const runStdio = <RAgent, RModel>(
570
+ options: Omit<Options<RAgent, RModel>, "send">,
571
+ ): Effect.Effect<
572
+ void,
573
+ PlatformError.PlatformError,
574
+ Stdio.Stdio | KeyValueStore.KeyValueStore | RAgent | RModel
575
+ > =>
576
+ Effect.gen(function* () {
577
+ const stdio = yield* Stdio.Stdio
578
+ const output = yield* Queue.make<string, Cause.Done>()
579
+ const writer = yield* Stream.fromQueue(output).pipe(
580
+ Stream.map((line) => `${line}\n`),
581
+ Stream.run(stdio.stdout()),
582
+ Effect.forkScoped,
583
+ )
584
+ const server = yield* make({
585
+ ...options,
586
+ send: (message) =>
587
+ Queue.offer(output, JSON.stringify(message)).pipe(Effect.asVoid),
588
+ })
589
+ yield* stdio.stdin.pipe(
590
+ Stream.decodeText,
591
+ Stream.splitLines,
592
+ Stream.runForEach((line) => Effect.forkScoped(server.handle(line))),
593
+ )
594
+ yield* Queue.end(output)
595
+ yield* Fiber.join(writer)
596
+ }).pipe(Effect.scoped)
@@ -29,7 +29,7 @@ import * as Scope from "effect/Scope"
29
29
  import * as Fiber from "effect/Fiber"
30
30
  import * as Console from "effect/Console"
31
31
  import * as Exit from "effect/Exit"
32
- import { pipe, flow, identity } from "effect/Function"
32
+ import { pipe, identity } from "effect/Function"
33
33
  import * as RpcClient from "effect/unstable/rpc/RpcClient"
34
34
  import * as Layer from "effect/Layer"
35
35
  import * as RpcServer from "effect/unstable/rpc/RpcServer"
@@ -131,18 +131,15 @@ export const makeLocal = Effect.fnUntraced(function* <
131
131
  const handlerScope = Scope.makeUnsafe("parallel")
132
132
  const trackFiber = Fiber.runIn(handlerScope)
133
133
 
134
- const taskServices = Context.mutate(
135
- Context.empty(),
136
- flow(
137
- Context.add(TaskCompleter, opts.onTaskComplete),
138
- Context.add(CurrentDirectory, options.directory),
139
- Context.add(SubagentExecutor, opts.onSubagent),
140
- Context.add(Console.Console, console),
141
- Context.add(References.CurrentLogAnnotations, {}),
142
- Option.isSome(search)
143
- ? Context.add(SemanticSearch, search.value)
144
- : identity,
145
- ),
134
+ const taskServices = Context.empty().pipe(
135
+ Context.add(TaskCompleter, opts.onTaskComplete),
136
+ Context.add(CurrentDirectory, options.directory),
137
+ Context.add(SubagentExecutor, opts.onSubagent),
138
+ Context.add(Console.Console, console),
139
+ Context.add(References.CurrentLogAnnotations, {}),
140
+ Option.isSome(search)
141
+ ? Context.add(SemanticSearch, search.value)
142
+ : identity,
146
143
  )
147
144
 
148
145
  yield* Effect.gen(function* () {
@@ -218,17 +215,14 @@ export const makeLocal = Effect.fnUntraced(function* <
218
215
  return Effect.die(new Error(`Unknown tool: ${opts.tool}`))
219
216
  }
220
217
 
221
- const taskServices = Context.mutate(
222
- handler.context,
223
- flow(
224
- Context.add(TaskCompleter, () => Effect.void),
225
- Context.add(CurrentDirectory, options.directory),
226
- Context.add(SubagentExecutor, () => Effect.succeed("")),
227
- Context.add(References.CurrentLogAnnotations, {}),
228
- Option.isSome(search)
229
- ? Context.add(SemanticSearch, search.value)
230
- : identity,
231
- ),
218
+ const taskServices = handler.context.pipe(
219
+ Context.add(TaskCompleter, () => Effect.void),
220
+ Context.add(CurrentDirectory, options.directory),
221
+ Context.add(SubagentExecutor, () => Effect.succeed("")),
222
+ Context.add(References.CurrentLogAnnotations, {}),
223
+ Option.isSome(search)
224
+ ? Context.add(SemanticSearch, search.value)
225
+ : identity,
232
226
  )
233
227
 
234
228
  const encodeSuccess = Schema.encodeUnknownEffect(
@@ -196,7 +196,7 @@ export const Output = Schema.Union([
196
196
  * @since 1.0.0
197
197
  * @category Output
198
198
  */
199
- export class AgentFinished extends Schema.TaggedErrorClass<AgentFinished>()(
199
+ export class AgentFinished extends Schema.TaggedError<AgentFinished>()(
200
200
  "AgentFinished",
201
201
  {
202
202
  summary: Schema.String,
package/src/ChunkRepo.ts CHANGED
@@ -121,7 +121,7 @@ export class ChunkRepo extends Context.Service<
121
121
  * @since 1.0.0
122
122
  * @category Errors
123
123
  */
124
- export class ChunkRepoError extends Schema.TaggedErrorClass<ChunkRepoError>()(
124
+ export class ChunkRepoError extends Schema.TaggedError<ChunkRepoError>()(
125
125
  "ChunkRepoError",
126
126
  {
127
127
  reason: Schema.Union([SqlError.SqlError]),
@@ -910,7 +910,7 @@ export const layer: Layer.Layer<
910
910
  entries
911
911
  .map((entry) => normalizePath(entry.trim()))
912
912
  .filter((entry) => entry.length > 0 && isMeaningfulFile(entry))
913
- .sort((left, right) => left.localeCompare(right)),
913
+ .toSorted((left, right) => left.localeCompare(right)),
914
914
  ),
915
915
  Effect.orDie,
916
916
  )
package/src/CodexAuth.ts CHANGED
@@ -47,7 +47,7 @@ export class TokenData extends Schema.Class<TokenData>(
47
47
  }
48
48
  }
49
49
 
50
- export class CodexAuthError extends Schema.TaggedErrorClass<CodexAuthError>()(
50
+ export class CodexAuthError extends Schema.TaggedError<CodexAuthError>()(
51
51
  "CodexAuthError",
52
52
  {
53
53
  reason: Schema.Literals([
@@ -44,7 +44,7 @@ export class TokenData extends Schema.Class<TokenData>(
44
44
  }
45
45
  }
46
46
 
47
- export class GithubCopilotAuthError extends Schema.TaggedErrorClass<GithubCopilotAuthError>()(
47
+ export class GithubCopilotAuthError extends Schema.TaggedError<GithubCopilotAuthError>()(
48
48
  "GithubCopilotAuthError",
49
49
  {
50
50
  reason: Schema.Literal("DeviceFlowFailed"),