clanka 0.5.2 → 0.6.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.map +1 -1
- package/dist/Acp.js +195 -16
- package/dist/Acp.js.map +1 -1
- package/dist/AcpImage.test.d.ts +2 -0
- package/dist/AcpImage.test.d.ts.map +1 -0
- package/dist/AcpImage.test.js +470 -0
- package/dist/AcpImage.test.js.map +1 -0
- package/dist/Agent.d.ts +9 -2
- package/dist/Agent.d.ts.map +1 -1
- package/dist/Agent.js +43 -6
- package/dist/Agent.js.map +1 -1
- package/dist/AgentExecutor.d.ts +27 -5
- package/dist/AgentExecutor.d.ts.map +1 -1
- package/dist/AgentExecutor.js +29 -4
- package/dist/AgentExecutor.js.map +1 -1
- package/dist/AgentExecutorImage.test.d.ts +2 -0
- package/dist/AgentExecutorImage.test.d.ts.map +1 -0
- package/dist/AgentExecutorImage.test.js +63 -0
- package/dist/AgentExecutorImage.test.js.map +1 -0
- package/dist/AgentImage.test.d.ts +2 -0
- package/dist/AgentImage.test.d.ts.map +1 -0
- package/dist/AgentImage.test.js +176 -0
- package/dist/AgentImage.test.js.map +1 -0
- package/dist/AgentTools.d.ts +25 -5
- package/dist/AgentTools.d.ts.map +1 -1
- package/dist/AgentTools.js +40 -4
- package/dist/AgentTools.js.map +1 -1
- package/dist/AgentToolsImage.test.d.ts +2 -0
- package/dist/AgentToolsImage.test.d.ts.map +1 -0
- package/dist/AgentToolsImage.test.js +158 -0
- package/dist/AgentToolsImage.test.js.map +1 -0
- package/dist/Codex.js +6 -1
- package/dist/Codex.js.map +1 -1
- package/dist/Compaction.d.ts.map +1 -1
- package/dist/Compaction.js +16 -1
- package/dist/Compaction.js.map +1 -1
- package/dist/CompactionImage.test.d.ts +2 -0
- package/dist/CompactionImage.test.d.ts.map +1 -0
- package/dist/CompactionImage.test.js +105 -0
- package/dist/CompactionImage.test.js.map +1 -0
- package/dist/Copilot.d.ts.map +1 -1
- package/dist/Copilot.js +5 -1
- package/dist/Copilot.js.map +1 -1
- package/dist/Image.d.ts +192 -0
- package/dist/Image.d.ts.map +1 -0
- package/dist/Image.js +586 -0
- package/dist/Image.js.map +1 -0
- package/dist/Image.test.d.ts +2 -0
- package/dist/Image.test.d.ts.map +1 -0
- package/dist/Image.test.js +142 -0
- package/dist/Image.test.js.map +1 -0
- package/dist/fixtures/TestImages.d.ts +73 -0
- package/dist/fixtures/TestImages.d.ts.map +1 -0
- package/dist/fixtures/TestImages.js +263 -0
- package/dist/fixtures/TestImages.js.map +1 -0
- 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 +2 -1
- package/src/Acp.ts +274 -21
- package/src/AcpImage.test.ts +676 -0
- package/src/Agent.ts +69 -7
- package/src/AgentExecutor.ts +51 -3
- package/src/AgentExecutorImage.test.ts +103 -0
- package/src/AgentImage.test.ts +295 -0
- package/src/AgentTools.ts +78 -7
- package/src/AgentToolsImage.test.ts +302 -0
- package/src/Codex.ts +6 -1
- package/src/Compaction.ts +16 -1
- package/src/CompactionImage.test.ts +142 -0
- package/src/Copilot.ts +5 -1
- package/src/Image.test.ts +220 -0
- package/src/Image.ts +704 -0
- package/src/fixtures/TestImages.ts +287 -0
- package/src/index.ts +5 -0
package/src/Acp.ts
CHANGED
|
@@ -11,11 +11,14 @@
|
|
|
11
11
|
import { randomUUID } from "node:crypto"
|
|
12
12
|
import * as Cause from "effect/Cause"
|
|
13
13
|
import * as Effect from "effect/Effect"
|
|
14
|
+
import * as Encoding from "effect/Encoding"
|
|
14
15
|
import * as Exit from "effect/Exit"
|
|
15
16
|
import * as Fiber from "effect/Fiber"
|
|
17
|
+
import * as FileSystem from "effect/FileSystem"
|
|
16
18
|
import type * as Layer from "effect/Layer"
|
|
17
19
|
import * as MutableRef from "effect/MutableRef"
|
|
18
20
|
import * as Option from "effect/Option"
|
|
21
|
+
import * as Path from "effect/Path"
|
|
19
22
|
import type * as PlatformError from "effect/PlatformError"
|
|
20
23
|
import * as Queue from "effect/Queue"
|
|
21
24
|
import * as Schema from "effect/Schema"
|
|
@@ -25,10 +28,12 @@ import * as Stream from "effect/Stream"
|
|
|
25
28
|
import type * as LanguageModel from "effect/unstable/ai/LanguageModel"
|
|
26
29
|
import type * as Model from "effect/unstable/ai/Model"
|
|
27
30
|
import * as Prompt from "effect/unstable/ai/Prompt"
|
|
31
|
+
import * as HttpClient from "effect/unstable/http/HttpClient"
|
|
28
32
|
import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore"
|
|
29
33
|
import type * as Agent from "./Agent.ts"
|
|
30
34
|
import type * as AgentOutput from "./AgentOutput.ts"
|
|
31
35
|
import * as Compaction from "./Compaction.ts"
|
|
36
|
+
import * as Image from "./Image.ts"
|
|
32
37
|
|
|
33
38
|
/**
|
|
34
39
|
* @since 1.0.0
|
|
@@ -139,21 +144,36 @@ const TextBlock = Schema.Struct({
|
|
|
139
144
|
text: Schema.String,
|
|
140
145
|
})
|
|
141
146
|
|
|
147
|
+
const ImageBlock = Schema.Struct({
|
|
148
|
+
type: Schema.Literal("image"),
|
|
149
|
+
data: Schema.String,
|
|
150
|
+
mimeType: Schema.String,
|
|
151
|
+
uri: Schema.optionalKey(Schema.String),
|
|
152
|
+
})
|
|
153
|
+
|
|
142
154
|
const ResourceLinkBlock = Schema.Struct({
|
|
143
155
|
type: Schema.Literal("resource_link"),
|
|
144
156
|
uri: Schema.String,
|
|
145
157
|
name: Schema.optionalKey(Schema.String),
|
|
158
|
+
mimeType: Schema.optionalKey(Schema.String),
|
|
146
159
|
})
|
|
147
160
|
|
|
148
161
|
const ResourceBlock = Schema.Struct({
|
|
149
162
|
type: Schema.Literal("resource"),
|
|
150
163
|
resource: Schema.Struct({
|
|
151
164
|
uri: Schema.String,
|
|
165
|
+
mimeType: Schema.optionalKey(Schema.String),
|
|
152
166
|
text: Schema.optionalKey(Schema.String),
|
|
167
|
+
blob: Schema.optionalKey(Schema.String),
|
|
153
168
|
}),
|
|
154
169
|
})
|
|
155
170
|
|
|
156
|
-
const ContentBlock = Schema.Union([
|
|
171
|
+
const ContentBlock = Schema.Union([
|
|
172
|
+
TextBlock,
|
|
173
|
+
ImageBlock,
|
|
174
|
+
ResourceLinkBlock,
|
|
175
|
+
ResourceBlock,
|
|
176
|
+
])
|
|
157
177
|
|
|
158
178
|
const PromptParams = Schema.Struct({
|
|
159
179
|
sessionId: Schema.String,
|
|
@@ -177,26 +197,42 @@ const toRpcError = (cause: Cause.Cause<unknown>) => {
|
|
|
177
197
|
: new RpcError({ code: -32603, message: Cause.pretty(cause) })
|
|
178
198
|
}
|
|
179
199
|
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
200
|
+
const renderLink = (uri: string, name?: string) => `[${name ?? uri}](${uri})`
|
|
201
|
+
|
|
202
|
+
// Covers the request and entire body read.
|
|
203
|
+
const remoteImageTimeout = "30 seconds"
|
|
204
|
+
|
|
205
|
+
const invalidParams = (message: string) =>
|
|
206
|
+
new RpcError({ code: -32602, message })
|
|
207
|
+
|
|
208
|
+
const decodeBase64 = (data: string, what: string) => {
|
|
209
|
+
if (data.length > Math.ceil(Image.maxInputBytes / 3) * 4) {
|
|
210
|
+
return Effect.fail(
|
|
211
|
+
invalidParams(
|
|
212
|
+
`Image data in ${what} exceeds the ${Image.maxInputBytes} byte input limit`,
|
|
213
|
+
),
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
const decoded = Encoding.decodeBase64(data)
|
|
217
|
+
return decoded._tag === "Success"
|
|
218
|
+
? Effect.succeed(decoded.success)
|
|
219
|
+
: Effect.fail(invalidParams(`Invalid base64 image data in ${what}`))
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const fileNameOf = (uri: string | undefined): string | undefined => {
|
|
223
|
+
if (uri === undefined) return undefined
|
|
224
|
+
return uri.split(/[\\/]/).findLast((part) => part.length > 0)
|
|
225
|
+
}
|
|
197
226
|
|
|
198
227
|
const textContent = (text: string) => ({ type: "text", text })
|
|
199
228
|
|
|
229
|
+
const imageContent = (part: Prompt.FilePart) =>
|
|
230
|
+
Option.map(Image.partBytes(part), (bytes) => ({
|
|
231
|
+
type: "image",
|
|
232
|
+
data: Encoding.encodeBase64(bytes),
|
|
233
|
+
mimeType: part.mediaType,
|
|
234
|
+
}))
|
|
235
|
+
|
|
200
236
|
const historyUpdates = (history: Prompt.Prompt) => {
|
|
201
237
|
const updates: Array<object> = []
|
|
202
238
|
for (const message of history.content) {
|
|
@@ -207,6 +243,11 @@ const historyUpdates = (history: Prompt.Prompt) => {
|
|
|
207
243
|
for (const part of message.content) {
|
|
208
244
|
if (part.type === "text") {
|
|
209
245
|
updates.push({ sessionUpdate, content: textContent(part.text) })
|
|
246
|
+
} else if (message.role === "user" && Image.isImagePart(part)) {
|
|
247
|
+
const content = imageContent(part)
|
|
248
|
+
if (Option.isSome(content)) {
|
|
249
|
+
updates.push({ sessionUpdate, content: content.value })
|
|
250
|
+
}
|
|
210
251
|
}
|
|
211
252
|
}
|
|
212
253
|
}
|
|
@@ -234,6 +275,12 @@ export const make = Effect.fnUntraced(function* <RAgent, RModel>(
|
|
|
234
275
|
KeyValueStore.KeyValueStore | Scope.Scope
|
|
235
276
|
> {
|
|
236
277
|
const kvs = yield* KeyValueStore.KeyValueStore
|
|
278
|
+
// Optional: `https:` images need an HttpClient and `file:` images need a
|
|
279
|
+
// FileSystem and Path. Inline (base64) images work without either. When a
|
|
280
|
+
// service is missing the corresponding block is rejected, never fetched.
|
|
281
|
+
const http = yield* Effect.serviceOption(HttpClient.HttpClient)
|
|
282
|
+
const fs = yield* Effect.serviceOption(FileSystem.FileSystem)
|
|
283
|
+
const path = yield* Effect.serviceOption(Path.Path)
|
|
237
284
|
const store = KeyValueStore.toSchemaStore(
|
|
238
285
|
KeyValueStore.prefix(kvs, "session-"),
|
|
239
286
|
SessionRecord,
|
|
@@ -307,9 +354,212 @@ export const make = Effect.fnUntraced(function* <RAgent, RModel>(
|
|
|
307
354
|
{ discard: true },
|
|
308
355
|
)
|
|
309
356
|
|
|
357
|
+
// ---------------------------------------------------------------------
|
|
358
|
+
// Prompt content blocks
|
|
359
|
+
// ---------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Fetch an `http(s):` image. Any transport or non-2xx failure rejects the
|
|
363
|
+
* prompt; nothing is retried here.
|
|
364
|
+
*/
|
|
365
|
+
const fetchImage = Effect.fnUntraced(
|
|
366
|
+
function* (uri: string) {
|
|
367
|
+
if (Option.isNone(http)) {
|
|
368
|
+
return yield* invalidParams(
|
|
369
|
+
`Cannot fetch image ${uri}: no HttpClient available`,
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
const response = yield* HttpClient.withScope(http.value).get(uri)
|
|
373
|
+
if (response.status < 200 || response.status >= 300) {
|
|
374
|
+
return yield* invalidParams(
|
|
375
|
+
`Failed to fetch image ${uri}: HTTP ${response.status}`,
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
const tooLarge = () =>
|
|
379
|
+
invalidParams(
|
|
380
|
+
`Image ${uri} exceeds the ${Image.maxInputBytes} byte download limit`,
|
|
381
|
+
)
|
|
382
|
+
if (Number(response.headers["content-length"]) > Image.maxInputBytes) {
|
|
383
|
+
return yield* tooLarge()
|
|
384
|
+
}
|
|
385
|
+
return yield* Image.collectInput(response.stream, tooLarge)
|
|
386
|
+
},
|
|
387
|
+
Effect.scoped,
|
|
388
|
+
Effect.timeout(remoteImageTimeout),
|
|
389
|
+
(effect, uri) =>
|
|
390
|
+
effect.pipe(
|
|
391
|
+
Effect.mapError((error) =>
|
|
392
|
+
error instanceof RpcError
|
|
393
|
+
? error
|
|
394
|
+
: invalidParams(`Failed to fetch image ${uri}: ${error.message}`),
|
|
395
|
+
),
|
|
396
|
+
),
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Read a `file:` image, but only when its real path (symlinks resolved)
|
|
401
|
+
* is inside the session directory (symlinks resolved as well).
|
|
402
|
+
*/
|
|
403
|
+
const readLocalImage = (
|
|
404
|
+
cwd: string,
|
|
405
|
+
uri: string,
|
|
406
|
+
): Effect.Effect<Uint8Array, RpcError> =>
|
|
407
|
+
Option.match(Option.all([fs, path]), {
|
|
408
|
+
onNone: () =>
|
|
409
|
+
Effect.fail(
|
|
410
|
+
invalidParams(`Cannot read image ${uri}: no FileSystem available`),
|
|
411
|
+
),
|
|
412
|
+
onSome: ([fs, path]) =>
|
|
413
|
+
Effect.gen(function* () {
|
|
414
|
+
const url = yield* Effect.try({
|
|
415
|
+
try: () => new URL(uri),
|
|
416
|
+
catch: () => invalidParams(`Invalid file URI: ${uri}`),
|
|
417
|
+
})
|
|
418
|
+
const filePath = yield* path
|
|
419
|
+
.fromFileUrl(url)
|
|
420
|
+
.pipe(
|
|
421
|
+
Effect.mapError(() => invalidParams(`Invalid file URI: ${uri}`)),
|
|
422
|
+
)
|
|
423
|
+
const resolved = path.resolve(cwd, filePath)
|
|
424
|
+
const [realCwd, real] = yield* Effect.all([
|
|
425
|
+
fs.realPath(cwd),
|
|
426
|
+
fs.realPath(resolved),
|
|
427
|
+
]).pipe(
|
|
428
|
+
Effect.mapError(() =>
|
|
429
|
+
invalidParams(`Image file not found: ${uri}`),
|
|
430
|
+
),
|
|
431
|
+
)
|
|
432
|
+
if (real !== realCwd && !real.startsWith(realCwd + path.sep)) {
|
|
433
|
+
return yield* invalidParams(
|
|
434
|
+
`Image file is outside the session directory: ${uri}`,
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
return yield* Image.readBoundedFile(fs, real).pipe(
|
|
438
|
+
Effect.mapError((error) =>
|
|
439
|
+
invalidParams(`Failed to read image ${uri}: ${error.message}`),
|
|
440
|
+
),
|
|
441
|
+
)
|
|
442
|
+
}),
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
const resolveImageUri = (
|
|
446
|
+
cwd: string,
|
|
447
|
+
uri: string,
|
|
448
|
+
): Effect.Effect<Uint8Array, RpcError> => {
|
|
449
|
+
if (/^https?:\/\//i.test(uri)) return fetchImage(uri)
|
|
450
|
+
if (/^file:/i.test(uri)) return readLocalImage(cwd, uri)
|
|
451
|
+
return Effect.fail(invalidParams(`Unsupported image URI scheme: ${uri}`))
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Turn raw image bytes into a prompt part, sniffing the real type and
|
|
456
|
+
* applying the shared size limits.
|
|
457
|
+
*/
|
|
458
|
+
const imagePart = Effect.fnUntraced(function* (
|
|
459
|
+
bytes: Uint8Array,
|
|
460
|
+
declared: string | undefined,
|
|
461
|
+
fileName: string | undefined,
|
|
462
|
+
) {
|
|
463
|
+
const mediaType = Option.getOrElse(Image.mediaTypeFromBytes(bytes), () =>
|
|
464
|
+
declared !== undefined && Image.isImageMediaType(declared)
|
|
465
|
+
? declared
|
|
466
|
+
: undefined,
|
|
467
|
+
)
|
|
468
|
+
if (mediaType === undefined) {
|
|
469
|
+
return yield* invalidParams(
|
|
470
|
+
`Unsupported image type${declared === undefined ? "" : `: ${declared}`}`,
|
|
471
|
+
)
|
|
472
|
+
}
|
|
473
|
+
const prepared = yield* Image.prepare({ data: bytes, mediaType }).pipe(
|
|
474
|
+
Effect.mapError((error) => invalidParams(error.message)),
|
|
475
|
+
)
|
|
476
|
+
return Prompt.makePart("file", {
|
|
477
|
+
mediaType: prepared.mediaType,
|
|
478
|
+
...(fileName === undefined ? {} : { fileName }),
|
|
479
|
+
data: prepared.data,
|
|
480
|
+
})
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Map ACP content blocks onto one user message. Text-like blocks are
|
|
485
|
+
* joined into text parts; image blocks become `file` parts in place.
|
|
486
|
+
*/
|
|
487
|
+
const promptFromBlocks = Effect.fnUntraced(function* (
|
|
488
|
+
cwd: string,
|
|
489
|
+
blocks: ReadonlyArray<typeof ContentBlock.Type>,
|
|
490
|
+
): Effect.fn.Return<Prompt.RawInput, RpcError> {
|
|
491
|
+
const parts: Array<Prompt.UserMessagePart> = []
|
|
492
|
+
let text: Array<string> = []
|
|
493
|
+
const flush = () => {
|
|
494
|
+
if (text.length === 0) return
|
|
495
|
+
parts.push(Prompt.makePart("text", { text: text.join("\n\n") }))
|
|
496
|
+
text = []
|
|
497
|
+
}
|
|
498
|
+
const addImage = (part: Prompt.FilePart) => {
|
|
499
|
+
flush()
|
|
500
|
+
parts.push(part)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
for (const block of blocks) {
|
|
504
|
+
switch (block.type) {
|
|
505
|
+
case "text":
|
|
506
|
+
text.push(block.text)
|
|
507
|
+
break
|
|
508
|
+
case "image": {
|
|
509
|
+
const bytes =
|
|
510
|
+
block.data.length > 0
|
|
511
|
+
? yield* decodeBase64(block.data, "image block")
|
|
512
|
+
: block.uri !== undefined
|
|
513
|
+
? yield* resolveImageUri(cwd, block.uri)
|
|
514
|
+
: yield* invalidParams("Image block has no data")
|
|
515
|
+
addImage(
|
|
516
|
+
yield* imagePart(bytes, block.mimeType, fileNameOf(block.uri)),
|
|
517
|
+
)
|
|
518
|
+
break
|
|
519
|
+
}
|
|
520
|
+
case "resource_link": {
|
|
521
|
+
if (
|
|
522
|
+
block.mimeType !== undefined &&
|
|
523
|
+
Image.isImageMediaType(block.mimeType)
|
|
524
|
+
) {
|
|
525
|
+
const bytes = yield* resolveImageUri(cwd, block.uri)
|
|
526
|
+
addImage(
|
|
527
|
+
yield* imagePart(
|
|
528
|
+
bytes,
|
|
529
|
+
block.mimeType,
|
|
530
|
+
block.name ?? fileNameOf(block.uri),
|
|
531
|
+
),
|
|
532
|
+
)
|
|
533
|
+
} else {
|
|
534
|
+
text.push(renderLink(block.uri, block.name))
|
|
535
|
+
}
|
|
536
|
+
break
|
|
537
|
+
}
|
|
538
|
+
case "resource": {
|
|
539
|
+
const { uri, mimeType, text: body, blob } = block.resource
|
|
540
|
+
if (
|
|
541
|
+
blob !== undefined &&
|
|
542
|
+
mimeType !== undefined &&
|
|
543
|
+
Image.isImageMediaType(mimeType)
|
|
544
|
+
) {
|
|
545
|
+
const bytes = yield* decodeBase64(blob, `resource ${uri}`)
|
|
546
|
+
addImage(yield* imagePart(bytes, mimeType, fileNameOf(uri)))
|
|
547
|
+
} else if (body !== undefined) {
|
|
548
|
+
text.push(`<resource uri="${uri}">\n${body}\n</resource>`)
|
|
549
|
+
} else {
|
|
550
|
+
text.push(renderLink(uri))
|
|
551
|
+
}
|
|
552
|
+
break
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
flush()
|
|
557
|
+
return [{ role: "user", content: parts }]
|
|
558
|
+
})
|
|
559
|
+
|
|
310
560
|
const runTurn = Effect.fnUntraced(function* (
|
|
311
561
|
session: Session,
|
|
312
|
-
prompt:
|
|
562
|
+
prompt: Prompt.RawInput,
|
|
313
563
|
) {
|
|
314
564
|
const stream = yield* session.agent.send({ prompt })
|
|
315
565
|
let script = ""
|
|
@@ -399,7 +649,7 @@ export const make = Effect.fnUntraced(function* <RAgent, RModel>(
|
|
|
399
649
|
protocolVersion: 1,
|
|
400
650
|
agentCapabilities: {
|
|
401
651
|
loadSession: true,
|
|
402
|
-
promptCapabilities: { image:
|
|
652
|
+
promptCapabilities: { image: true, audio: false, embeddedContext: true },
|
|
403
653
|
mcpCapabilities: { http: false, sse: false },
|
|
404
654
|
},
|
|
405
655
|
authMethods: [],
|
|
@@ -475,7 +725,10 @@ export const make = Effect.fnUntraced(function* <RAgent, RModel>(
|
|
|
475
725
|
})
|
|
476
726
|
}
|
|
477
727
|
const model = yield* requireModel(session.model)
|
|
478
|
-
|
|
728
|
+
// Resolve images before the turn starts, so a bad block rejects the
|
|
729
|
+
// request without a model call.
|
|
730
|
+
const input = yield* promptFromBlocks(session.cwd, prompt)
|
|
731
|
+
session.running = yield* runTurn(session, input).pipe(
|
|
479
732
|
Effect.provide(model),
|
|
480
733
|
Effect.scoped,
|
|
481
734
|
Effect.forkChild,
|