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.
- package/dist/Acp.d.ts +88 -0
- package/dist/Acp.d.ts.map +1 -0
- package/dist/Acp.js +387 -0
- package/dist/Acp.js.map +1 -0
- package/dist/Acp.test.d.ts +2 -0
- package/dist/Acp.test.d.ts.map +1 -0
- package/dist/Acp.test.js +179 -0
- package/dist/Acp.test.js.map +1 -0
- package/dist/AgentExecutor.d.ts +36 -36
- package/dist/AgentExecutor.d.ts.map +1 -1
- package/dist/AgentExecutor.js +5 -5
- package/dist/AgentExecutor.js.map +1 -1
- package/dist/AgentOutput.js +1 -1
- package/dist/AgentOutput.js.map +1 -1
- package/dist/ChunkRepo.js +1 -1
- package/dist/ChunkRepo.js.map +1 -1
- package/dist/CodeChunker.js +1 -1
- package/dist/CodeChunker.js.map +1 -1
- package/dist/CodexAuth.js +1 -1
- package/dist/CodexAuth.js.map +1 -1
- package/dist/CopilotAuth.js +1 -1
- package/dist/CopilotAuth.js.map +1 -1
- package/dist/DeviceCodeHandler.d.ts +2 -1
- package/dist/DeviceCodeHandler.d.ts.map +1 -1
- package/dist/DeviceCodeHandler.js +4 -0
- package/dist/DeviceCodeHandler.js.map +1 -1
- package/dist/ExaSearch.js +1 -1
- package/dist/ExaSearch.js.map +1 -1
- package/dist/McpClient.js +1 -1
- package/dist/McpClient.js.map +1 -1
- package/dist/SemanticSearch.d.ts +2 -2
- package/dist/SemanticSearch.d.ts.map +1 -1
- package/dist/SemanticSearch.js.map +1 -1
- package/dist/TypeBuilder.js +1 -1
- package/dist/TypeBuilder.js.map +1 -1
- package/dist/cli.js +40 -29
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/package.json +21 -21
- package/src/Acp.test.ts +288 -0
- package/src/Acp.ts +596 -0
- package/src/AgentExecutor.ts +18 -24
- package/src/AgentOutput.ts +1 -1
- package/src/ChunkRepo.ts +1 -1
- package/src/CodeChunker.ts +1 -1
- package/src/CodexAuth.ts +1 -1
- package/src/CopilotAuth.ts +1 -1
- package/src/DeviceCodeHandler.ts +8 -1
- package/src/ExaSearch.ts +1 -1
- package/src/McpClient.ts +1 -1
- package/src/SemanticSearch.ts +2 -2
- package/src/TypeBuilder.ts +1 -1
- package/src/cli.ts +100 -46
- 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)
|
package/src/AgentExecutor.ts
CHANGED
|
@@ -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,
|
|
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.
|
|
135
|
-
Context.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
Context.add(
|
|
142
|
-
|
|
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 =
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
Context.add(
|
|
228
|
-
|
|
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(
|
package/src/AgentOutput.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
124
|
+
export class ChunkRepoError extends Schema.TaggedError<ChunkRepoError>()(
|
|
125
125
|
"ChunkRepoError",
|
|
126
126
|
{
|
|
127
127
|
reason: Schema.Union([SqlError.SqlError]),
|
package/src/CodeChunker.ts
CHANGED
|
@@ -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
|
-
.
|
|
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.
|
|
50
|
+
export class CodexAuthError extends Schema.TaggedError<CodexAuthError>()(
|
|
51
51
|
"CodexAuthError",
|
|
52
52
|
{
|
|
53
53
|
reason: Schema.Literals([
|
package/src/CopilotAuth.ts
CHANGED
|
@@ -44,7 +44,7 @@ export class TokenData extends Schema.Class<TokenData>(
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export class GithubCopilotAuthError extends Schema.
|
|
47
|
+
export class GithubCopilotAuthError extends Schema.TaggedError<GithubCopilotAuthError>()(
|
|
48
48
|
"GithubCopilotAuthError",
|
|
49
49
|
{
|
|
50
50
|
reason: Schema.Literal("DeviceFlowFailed"),
|