clanka 0.0.2 → 0.0.4

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.
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * @since 1.0.0
3
3
  */
4
- import { Sink } from "effect";
5
- import type { Output } from "./Agent.ts";
4
+ import { Stream } from "effect";
5
+ import { type Output, AgentFinished } from "./Agent.ts";
6
6
  /**
7
7
  * @since 1.0.0
8
8
  * @category Models
9
9
  */
10
- export type OutputFormatter<E = never, R = never> = Sink.Sink<void, Output, never, E, R>;
10
+ export type OutputFormatter<E = never, R = never> = (stream: Stream.Stream<Output, AgentFinished>) => Stream.Stream<string, E, R>;
11
11
  /**
12
12
  * @since 1.0.0
13
13
  * @category Pretty
@@ -1 +1 @@
1
- {"version":3,"file":"OutputFormatter.d.ts","sourceRoot":"","sources":["../src/OutputFormatter.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAU,IAAI,EAAU,MAAM,QAAQ,CAAA;AAC7C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAGxC;;;GAGG;AACH,MAAM,MAAM,eAAe,CAAC,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,KAAK,IAAI,IAAI,CAAC,IAAI,CAC3D,IAAI,EACJ,MAAM,EACN,KAAK,EACL,CAAC,EACD,CAAC,CACF,CAAA;AAyED;;;GAGG;AACH,eAAO,MAAM,MAAM,EAAE,eAAoC,CAAA"}
1
+ {"version":3,"file":"OutputFormatter.d.ts","sourceRoot":"","sources":["../src/OutputFormatter.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/B,OAAO,EAAE,KAAK,MAAM,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAGvD;;;GAGG;AACH,MAAM,MAAM,eAAe,CAAC,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,KAAK,IAAI,CAClD,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,KACzC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;AAEhC;;;GAGG;AACH,eAAO,MAAM,MAAM,EAAE,eAgDlB,CAAA"}
@@ -1,67 +1,56 @@
1
1
  /**
2
2
  * @since 1.0.0
3
3
  */
4
- import { Effect, Sink, Stream } from "effect";
4
+ import { Stream } from "effect";
5
+ import { AgentFinished } from "./Agent.js";
5
6
  import chalk from "chalk";
6
- const prettyPrefixed = (prefix) => Sink.suspend(() => {
7
- let hadReasoningDelta = false;
8
- return Sink.forEach((output) => {
9
- switch (output._tag) {
10
- case "SubagentPart": {
11
- console.log(chalkSubagentHeading(`${subagentIcon} Subagent #${output.id} starting (${output.modelAndProvider})`));
12
- console.log("");
13
- console.log(chalk.dim(output.prompt));
14
- console.log("");
15
- const prefix = chalk.magenta(`Subagent #${output.id}:`) + " ";
16
- return output.output.pipe(Stream.run(prettyPrefixed(prefix)), Effect.catch((finished) => {
17
- console.log(chalkSubagentHeading(`${subagentIcon} Subagent #${output.id} complete`));
18
- console.log("");
19
- console.log(finished.summary);
20
- console.log("");
21
- return Effect.void;
22
- }), Effect.forkChild);
23
- }
24
- case "ReasoningDelta": {
25
- process.stdout.write(output.delta);
26
- hadReasoningDelta = true;
27
- break;
28
- }
29
- case "ReasoningEnd": {
30
- if (hadReasoningDelta) {
31
- console.log("\n");
32
- hadReasoningDelta = false;
33
- }
34
- break;
35
- }
36
- case "ScriptStart": {
37
- console.log(prefix + chalkScriptHeading(`${scriptIcon} Executing script`));
38
- console.log("");
39
- console.log(chalk.dim(output.script));
40
- console.log("");
41
- break;
42
- }
43
- case "ScriptEnd": {
44
- console.log(prefix + chalkScriptHeading(`${scriptIcon} Script output`));
45
- console.log("");
46
- const lines = output.output.split("\n");
47
- const truncated = lines.length > 20
48
- ? lines.slice(0, 20).join("\n") + "\n... (truncated)"
49
- : output.output;
50
- console.log(chalk.dim(truncated));
51
- console.log(chalk.reset(""));
52
- break;
53
- }
54
- }
55
- return Effect.void;
56
- });
57
- });
58
7
  /**
59
8
  * @since 1.0.0
60
9
  * @category Pretty
61
10
  */
62
- export const pretty = prettyPrefixed("");
11
+ export const pretty = (stream) => stream.pipe(Stream.map((output) => {
12
+ let prefix = "";
13
+ if (output._tag === "SubagentPart") {
14
+ prefix = chalk.magenta(`Subagent #${output.id}:`) + " ";
15
+ output = output.part;
16
+ }
17
+ switch (output._tag) {
18
+ case "SubagentStart": {
19
+ return `${chalkSubagentHeading(`${subagentIcon} Subagent #${output.id} starting (${output.modelAndProvider})`)}
20
+
21
+ ${chalk.dim(output.prompt)}\n\n`;
22
+ }
23
+ case "SubagentComplete": {
24
+ return `${chalkSubagentHeading(`${subagentIcon} Subagent #${output.id} complete`)}
25
+
26
+ ${output.summary}\n\n`;
27
+ }
28
+ case "ReasoningStart": {
29
+ return (prefix + chalkReasoningHeading(`${thinkingIcon} Thinking:`) + " ");
30
+ }
31
+ case "ReasoningDelta": {
32
+ return output.delta;
33
+ }
34
+ case "ReasoningEnd": {
35
+ return "\n\n";
36
+ }
37
+ case "ScriptStart": {
38
+ return `${prefix}${chalkScriptHeading(`${scriptIcon} Executing script`)}\n\n${chalk.dim(output.script)}\n\n`;
39
+ }
40
+ case "ScriptEnd": {
41
+ const lines = output.output.split("\n");
42
+ const truncated = lines.length > 20
43
+ ? lines.slice(0, 20).join("\n") + "\n... (truncated)"
44
+ : output.output;
45
+ return `${prefix}${chalkScriptHeading(`${scriptIcon} Script output`)}\n\n${chalk.dim(truncated)}\n\n`;
46
+ }
47
+ }
48
+ }), Stream.catch((finished) => Stream.succeed(`\n${chalk.bold.green(`${doneIcon} Task complete:`)}\n\n${finished.summary}`)));
63
49
  const chalkScriptHeading = chalk.bold.blue;
64
50
  const chalkSubagentHeading = chalk.bold.magenta;
51
+ const chalkReasoningHeading = chalk.bold.yellow;
65
52
  const scriptIcon = "\u{f0bc1}";
66
53
  const subagentIcon = "\u{ee0d} ";
54
+ const thinkingIcon = "\u{f07f6}";
55
+ const doneIcon = "\u{eab2}";
67
56
  //# sourceMappingURL=OutputFormatter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"OutputFormatter.js","sourceRoot":"","sources":["../src/OutputFormatter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"OutputFormatter.js","sourceRoot":"","sources":["../src/OutputFormatter.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clanka",
3
3
  "type": "module",
4
- "version": "0.0.2",
4
+ "version": "0.0.4",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -10,13 +10,6 @@
10
10
  "./*": "./dist/*.js",
11
11
  "./package.json": "./package.json"
12
12
  },
13
- "scripts": {
14
- "prepare": "husky && effect-language-service patch",
15
- "check": "oxlint -c .oxlintrc.json --fix && pnpm tsgo --noEmit",
16
- "test": "vitest run",
17
- "build": "tsc -b tsconfig.json",
18
- "prepublishOnly": "pnpm build"
19
- },
20
13
  "files": [
21
14
  "dist",
22
15
  "src"
@@ -28,7 +21,6 @@
28
21
  "type": "git",
29
22
  "url": "https://github.com/tim-smart/clanka.git"
30
23
  },
31
- "packageManager": "pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48",
32
24
  "dependencies": {
33
25
  "@vscode/ripgrep": "^1.17.0",
34
26
  "chalk": "^5.6.2",
@@ -65,9 +57,9 @@
65
57
  "prettier --write"
66
58
  ]
67
59
  },
68
- "pnpm": {
69
- "onlyBuiltDependencies": [
70
- "@vscode/ripgrep"
71
- ]
60
+ "scripts": {
61
+ "check": "oxlint -c .oxlintrc.json --fix && pnpm tsc --noEmit",
62
+ "test": "vitest run",
63
+ "build": "tsc -b tsconfig.json"
72
64
  }
73
- }
65
+ }
package/src/Agent.ts CHANGED
@@ -3,7 +3,6 @@
3
3
  */
4
4
  import {
5
5
  Array,
6
- Data,
7
6
  Deferred,
8
7
  Effect,
9
8
  FileSystem,
@@ -39,6 +38,16 @@ import type { ChildProcessSpawner } from "effect/unstable/process"
39
38
  */
40
39
  export interface Agent {
41
40
  readonly output: Stream.Stream<Output, AgentFinished>
41
+
42
+ /**
43
+ * Send a message to the agent to steer its behavior. This is useful for
44
+ * providing feedback or new instructions while the agent is running.
45
+ *
46
+ * The effect will only complete once the message has been sent.
47
+ * Interrupting the effect will withdraw the message, so it will not be sent
48
+ * to the agent.
49
+ */
50
+ steer(message: string): Effect.Effect<void>
42
51
  }
43
52
 
44
53
  /**
@@ -111,62 +120,128 @@ export const make: <
111
120
  | ProviderName
112
121
  | ModelName
113
122
  >()
123
+ const pendingMessages = new Set<{
124
+ readonly message: string
125
+ readonly resume: (effect: Effect.Effect<void>) => void
126
+ }>()
114
127
 
115
128
  let system = yield* generateSystem(allTools)
129
+
130
+ const agentsMd = yield* pipe(
131
+ fs.readFileString(pathService.resolve(options.directory, "AGENTS.md")),
132
+ Effect.option,
133
+ )
134
+
135
+ if (Option.isSome(agentsMd)) {
136
+ system += `
137
+ # AGENTS.md
138
+
139
+ The following instructions are from ./AGENTS.md in the current directory.
140
+ You do not need to read this file again.
141
+
142
+ **ALWAYS follow these instructions when completing tasks**:
143
+
144
+ <!-- AGENTS.md start -->
145
+ ${agentsMd.value}
146
+ <!-- AGENTS.md end -->
147
+ `
148
+ }
149
+
116
150
  if (options.system) {
117
- system += `\n${options.system}`
151
+ system += `\n${options.system}\n`
118
152
  }
153
+
119
154
  const withSystemPrompt = OpenAiLanguageModel.withConfigOverride({
120
155
  store: false,
121
156
  instructions: system,
122
157
  })
123
158
 
124
- const agentsMd = yield* pipe(
125
- fs.readFileString(pathService.resolve(options.directory, "AGENTS.md")),
126
- Effect.option,
127
- )
159
+ let agentCounter = 0
128
160
 
129
- let subagentId = 0
161
+ const outputBuffer = new Map<number, Array<Output>>()
162
+ let currentOutputAgent: number | null = null
130
163
 
131
164
  const spawn: (
165
+ agentId: number,
132
166
  prompt: Prompt.Prompt,
133
167
  ) => Stream.Stream<
134
168
  Output,
135
169
  AgentFinished,
136
170
  LanguageModel.LanguageModel | ProviderName
137
- > = Effect.fnUntraced(function* (prompt) {
171
+ > = Effect.fnUntraced(function* (agentId, prompt) {
138
172
  const ai = yield* LanguageModel.LanguageModel
139
173
  const provider = yield* ProviderName
140
174
  const deferred = yield* Deferred.make<string>()
141
175
  const output = yield* Queue.make<Output, AgentFinished>()
142
176
 
177
+ function maybeSend(agentId: number, part: Output, lock = false) {
178
+ if (currentOutputAgent === null || currentOutputAgent === agentId) {
179
+ Queue.offerUnsafe(output, part)
180
+ if (lock) {
181
+ currentOutputAgent = agentId
182
+ }
183
+ return true
184
+ }
185
+ const state = outputBuffer.get(agentId) ?? []
186
+ state.push(part)
187
+ return false
188
+ }
189
+
190
+ function flushBuffer() {
191
+ currentOutputAgent = null
192
+ for (const [id, state] of outputBuffer) {
193
+ outputBuffer.delete(id)
194
+ Queue.offerAllUnsafe(output, state)
195
+ const lastPart = state[state.length - 1]!
196
+ if (lastPart._tag === "ReasoningDelta") {
197
+ currentOutputAgent = id
198
+ break
199
+ }
200
+ }
201
+ }
202
+
143
203
  const taskServices = SubagentContext.serviceMap({
144
204
  spawn: ({ prompt }) => {
145
- let id = ++subagentId
146
- return spawn(Prompt.make(prompt)).pipe(
147
- Stream.broadcast({
148
- capacity: "unbounded",
149
- }),
150
- Effect.flatMap(
151
- Effect.fnUntraced(function* (stream) {
152
- const provider = yield* ProviderName
153
- const model = yield* ModelName
205
+ let id = agentCounter++
206
+ const stream = spawn(
207
+ id,
208
+ Prompt.make(`You have been spawned using "subagent" to complete the following task:
209
+
210
+ ${prompt}`),
211
+ )
212
+ return Effect.gen(function* () {
213
+ const provider = yield* ProviderName
214
+ const model = yield* ModelName
215
+ Queue.offerUnsafe(
216
+ output,
217
+ new SubagentStart({ id, prompt, model, provider }),
218
+ )
219
+ return yield* stream.pipe(
220
+ Stream.runForEachArray((parts) => {
221
+ for (const part of parts) {
222
+ switch (part._tag) {
223
+ case "SubagentStart":
224
+ case "SubagentComplete":
225
+ case "SubagentPart":
226
+ break
227
+
228
+ default:
229
+ Queue.offerUnsafe(output, new SubagentPart({ id, part }))
230
+ break
231
+ }
232
+ }
233
+ return Effect.void
234
+ }),
235
+ Effect.as(""),
236
+ Effect.catch((finished) => {
154
237
  Queue.offerUnsafe(
155
238
  output,
156
- new SubagentPart({
157
- id,
158
- prompt,
159
- provider,
160
- model,
161
- output: stream,
162
- }),
239
+ new SubagentComplete({ id, summary: finished.summary }),
163
240
  )
164
- return yield* Stream.runDrain(stream)
241
+ return Effect.succeed(finished.summary)
165
242
  }),
166
- ),
167
- Effect.scoped,
168
- Effect.as(""),
169
- Effect.catch((e) => Effect.succeed(e.summary)),
243
+ )
244
+ }).pipe(
170
245
  options.subagentModel
171
246
  ? Effect.provide(Layer.orDie(options.subagentModel))
172
247
  : Effect.provideServices(services),
@@ -177,18 +252,6 @@ export const make: <
177
252
  ServiceMap.add(TaskCompleteDeferred, deferred),
178
253
  )
179
254
 
180
- prompt = Prompt.concat(
181
- prompt,
182
- agentsMd.pipe(
183
- Option.map((md) =>
184
- Prompt.make(`Here is a copy of ./AGENTS.md. ALWAYS follow these instructions when completing the above task:
185
-
186
- ${md}`),
187
- ),
188
- Option.getOrElse(() => Prompt.empty),
189
- ),
190
- )
191
-
192
255
  if (provider !== "openai") {
193
256
  prompt = Prompt.setSystem(prompt, system)
194
257
  }
@@ -197,7 +260,7 @@ ${md}`),
197
260
  yield* Effect.gen(function* () {
198
261
  while (true) {
199
262
  if (currentScript.length > 0) {
200
- Queue.offerUnsafe(output, new ScriptStart({ script: currentScript }))
263
+ maybeSend(agentId, new ScriptStart({ script: currentScript }))
201
264
  const result = yield* pipe(
202
265
  executor.execute({
203
266
  tools,
@@ -205,8 +268,10 @@ ${md}`),
205
268
  }),
206
269
  Stream.mkString,
207
270
  )
208
- Queue.offerUnsafe(output, new ScriptEnd({ output: result }))
209
- prompt = Prompt.concat(prompt, `Javascript output:\n\n${result}`)
271
+ maybeSend(agentId, new ScriptEnd({ output: result }))
272
+ prompt = Prompt.concat(prompt, [
273
+ { role: "assistant", content: `Javascript output:\n\n${result}` },
274
+ ])
210
275
  currentScript = ""
211
276
  }
212
277
 
@@ -218,15 +283,37 @@ ${md}`),
218
283
  return
219
284
  }
220
285
 
286
+ if (pendingMessages.size > 0) {
287
+ prompt = Prompt.concat(
288
+ prompt,
289
+ Array.Array.from(pendingMessages, ({ message, resume }) => {
290
+ resume(Effect.void)
291
+ return {
292
+ role: "user",
293
+ content: message,
294
+ }
295
+ }),
296
+ )
297
+ pendingMessages.clear()
298
+ }
299
+
221
300
  let response = Array.empty<StreamPart<{}>>()
301
+ let reasoningStarted = false
302
+ let hadReasoningDelta = false
222
303
  yield* pipe(
223
304
  ai.streamText({ prompt }),
224
- Stream.takeUntil(
225
- (part) =>
226
- part.type === "text-end" && currentScript.trim().length > 0,
227
- ),
305
+ Stream.takeUntil((part) => {
306
+ if (part.type === "text-end" && currentScript.trim().length > 0) {
307
+ return true
308
+ }
309
+ if (part.type === "reasoning-end" && pendingMessages.size > 0) {
310
+ return true
311
+ }
312
+ return false
313
+ }),
228
314
  Stream.runForEachArray((parts) => {
229
315
  response.push(...parts)
316
+
230
317
  for (const part of parts) {
231
318
  switch (part.type) {
232
319
  case "text-start":
@@ -236,16 +323,23 @@ ${md}`),
236
323
  currentScript += part.delta
237
324
  break
238
325
  case "reasoning-start":
239
- Queue.offerUnsafe(output, new ReasoningStart())
326
+ reasoningStarted = true
240
327
  break
241
328
  case "reasoning-delta":
242
- Queue.offerUnsafe(
243
- output,
244
- new ReasoningDelta({ delta: part.delta }),
245
- )
329
+ hadReasoningDelta = true
330
+ if (reasoningStarted) {
331
+ reasoningStarted = false
332
+ maybeSend(agentId, new ReasoningStart(), true)
333
+ }
334
+ maybeSend(agentId, new ReasoningDelta({ delta: part.delta }))
246
335
  break
247
336
  case "reasoning-end":
248
- Queue.offerUnsafe(output, new ReasoningEnd())
337
+ reasoningStarted = false
338
+ if (hadReasoningDelta) {
339
+ hadReasoningDelta = false
340
+ const sent = maybeSend(agentId, new ReasoningEnd())
341
+ if (sent) flushBuffer()
342
+ }
249
343
  break
250
344
  case "finish":
251
345
  // console.log("Tokens used:", part.usage, "\n")
@@ -274,7 +368,7 @@ ${md}`),
274
368
  return Stream.fromQueue(output)
275
369
  }, Stream.unwrap)
276
370
 
277
- const output = yield* spawn(Prompt.make(options.prompt)).pipe(
371
+ const output = yield* spawn(agentCounter++, Prompt.make(options.prompt)).pipe(
278
372
  Stream.broadcast({
279
373
  capacity: "unbounded",
280
374
  }),
@@ -282,6 +376,12 @@ ${md}`),
282
376
 
283
377
  return identity<Agent>({
284
378
  output,
379
+ steer: (message) =>
380
+ Effect.callback((resume) => {
381
+ const entry = { message, resume }
382
+ pendingMessages.add(entry)
383
+ return Effect.sync(() => pendingMessages.delete(entry))
384
+ }),
285
385
  })
286
386
  // oxlint-disable-next-line typescript/no-explicit-any
287
387
  }) as any
@@ -290,19 +390,17 @@ ${md}`),
290
390
  const generateSystem = Effect.fn(function* (tools: Toolkit.Toolkit<any>) {
291
391
  const renderer = yield* ToolkitRenderer
292
392
 
293
- return `# Who you are
294
-
295
- You are a professional software engineer. You are precise, thoughtful and concise. You make changes with care and always do the due diligence to ensure the best possible outcome. You make no mistakes.
296
-
297
- # Completing the task
393
+ return `You are a professional software engineer. You are precise, thoughtful and concise. You make changes with care and always do the due diligence to ensure the best possible outcome. You make no mistakes.
298
394
 
299
- To complete the task respond with javascript code that will be executed for you.
395
+ To do your job, only respond with javascript code that will be executed for you.
300
396
 
301
397
  - Do not add any markdown formatting, just code.
302
398
  - Use \`console.log\` to print any output you need.
303
399
  - Top level await is supported.
304
400
  - **Prefer using the functions provided** over the bash tool
305
401
 
402
+ **When you have completed your task**, call the "taskComplete" function with the final output.
403
+
306
404
  You have the following functions available to you:
307
405
 
308
406
  \`\`\`ts
@@ -338,9 +436,7 @@ Javascript output:
338
436
 
339
437
  - Use the current state of the codebase to inform your decisions. Don't look at git history unless explicity asked to.
340
438
  - Only add comments when necessary.
341
- - Repect the users AGENTS.md file and ALWAYS follow the instructions in it.
342
- - Use the "subagent" tool to delegate research / exploration tasks
343
- - When you have fully completed the task, call the "taskComplete" function
439
+ - Use the "subagent" tool to delegate large tasks / exploration. Run multiple subagents in parallel with Promise.all
344
440
  `
345
441
  })
346
442
 
@@ -407,7 +503,40 @@ export class AgentFinished extends Schema.TaggedErrorClass<AgentFinished>()(
407
503
  * @since 1.0.0
408
504
  * @category Output
409
505
  */
410
- export const OutputPart = Schema.Union([
506
+ export class SubagentStart extends Schema.TaggedClass<SubagentStart>()(
507
+ "SubagentStart",
508
+ {
509
+ id: Schema.Number,
510
+ prompt: Schema.String,
511
+ model: Schema.String,
512
+ provider: Schema.String,
513
+ },
514
+ ) {
515
+ get modelAndProvider() {
516
+ return `${this.provider}/${this.model}`
517
+ }
518
+ }
519
+
520
+ /**
521
+ * @since 1.0.0
522
+ * @category Output
523
+ */
524
+ export class SubagentComplete extends Schema.TaggedClass<SubagentComplete>()(
525
+ "SubagentComplete",
526
+ {
527
+ id: Schema.Number,
528
+ summary: Schema.String,
529
+ },
530
+ ) {}
531
+
532
+ export type ContentPart =
533
+ | ReasoningStart
534
+ | ReasoningDelta
535
+ | ReasoningEnd
536
+ | ScriptStart
537
+ | ScriptEnd
538
+
539
+ export const ContentPart = Schema.Union([
411
540
  ReasoningStart,
412
541
  ReasoningDelta,
413
542
  ReasoningEnd,
@@ -419,26 +548,35 @@ export const OutputPart = Schema.Union([
419
548
  * @since 1.0.0
420
549
  * @category Output
421
550
  */
422
- export type OutputPart = typeof OutputPart.Type
551
+ export class SubagentPart extends Schema.TaggedClass<SubagentPart>()(
552
+ "SubagentPart",
553
+ {
554
+ id: Schema.Number,
555
+ part: ContentPart,
556
+ },
557
+ ) {}
423
558
 
424
559
  /**
425
560
  * @since 1.0.0
426
561
  * @category Output
427
562
  */
428
- export class SubagentPart extends Data.TaggedError("SubagentPart")<{
429
- id: number
430
- prompt: string
431
- model: string
432
- provider: string
433
- output: Stream.Stream<Output, AgentFinished>
434
- }> {
435
- get modelAndProvider() {
436
- return `${this.provider}/${this.model}`
437
- }
438
- }
563
+ export type Output =
564
+ | ContentPart
565
+ | SubagentStart
566
+ | SubagentComplete
567
+ | SubagentPart
439
568
 
440
569
  /**
441
570
  * @since 1.0.0
442
571
  * @category Output
443
572
  */
444
- export type Output = OutputPart | SubagentPart
573
+ export const Output = Schema.Union([
574
+ ReasoningStart,
575
+ ReasoningDelta,
576
+ ReasoningEnd,
577
+ ScriptStart,
578
+ ScriptEnd,
579
+ SubagentStart,
580
+ SubagentComplete,
581
+ SubagentPart,
582
+ ])
package/src/AgentTools.ts CHANGED
@@ -16,7 +16,6 @@ import {
16
16
  import { Tool, Toolkit } from "effect/unstable/ai"
17
17
  import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
18
18
  import * as Glob from "glob"
19
- import * as Rg from "@vscode/ripgrep"
20
19
  import { parsePatch, patchChunks } from "./ApplyPatch.ts"
21
20
 
22
21
  /**
@@ -180,9 +179,9 @@ export const AgentTools = Toolkit.make(
180
179
  }),
181
180
  Tool.make("taskComplete", {
182
181
  description:
183
- "Only call this when you have fully completed the user's task. Provide a markdown summary of the work you have done.",
182
+ "Call this to send a final output message and end the current task.",
184
183
  parameters: Schema.String.annotate({
185
- identifier: "summary",
184
+ identifier: "output",
186
185
  }),
187
186
  dependencies: [TaskCompleteDeferred],
188
187
  }),
@@ -304,7 +303,7 @@ export const AgentToolHandlers = AgentTools.toLayer(
304
303
  args.push(options.pattern)
305
304
  let stream = pipe(
306
305
  spawner.streamLines(
307
- ChildProcess.make(Rg.rgPath, args, {
306
+ ChildProcess.make("rg", args, {
308
307
  cwd,
309
308
  stdin: "ignore",
310
309
  }),
package/src/Executor.ts CHANGED
@@ -96,7 +96,6 @@ ${options.script}
96
96
  }
97
97
  }).pipe(
98
98
  Effect.ensuring(Scope.close(handlerScope, Exit.void)),
99
- Effect.timeout("3 minutes"),
100
99
  Effect.catchCause(Effect.logFatal),
101
100
  Effect.provideService(Console.Console, console),
102
101
  Effect.ensuring(Queue.end(output)),