clanka 0.7.2 → 0.7.3

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clanka",
3
3
  "type": "module",
4
- "version": "0.7.2",
4
+ "version": "0.7.3",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
package/src/Agent.test.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import { assert, describe, it } from "@effect/vitest"
2
+ import * as NodeServices from "@effect/platform-node/NodeServices"
3
+ import * as FileSystem from "effect/FileSystem"
4
+ import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"
2
5
  import * as Effect from "effect/Effect"
3
6
  import * as Duration from "effect/Duration"
4
7
  import * as Exit from "effect/Exit"
@@ -404,6 +407,121 @@ const assertCompactedShape = (
404
407
  return prompt.content.slice(2)
405
408
  }
406
409
 
410
+ const localExecutorLayer = Layer.unwrap(
411
+ Effect.gen(function* () {
412
+ const fs = yield* FileSystem.FileSystem
413
+ const directory = yield* fs.makeTempDirectoryScoped()
414
+ return AgentExecutor.layerLocal({ directory })
415
+ }),
416
+ ).pipe(Layer.provide(Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer)))
417
+
418
+ const invalidCompletions = [
419
+ 'taskComplete({ output: "done" })',
420
+ "taskComplete()",
421
+ "taskComplete(undefined)",
422
+ "taskComplete(null)",
423
+ "taskComplete(42)",
424
+ "taskComplete(true)",
425
+ 'taskComplete(["done"])',
426
+ 'taskComplete({ summary: "done" })',
427
+ ]
428
+
429
+ it.layer(localExecutorLayer, { excludeTestServices: true })(
430
+ "taskComplete through the real VM executor",
431
+ (it) => {
432
+ for (const summary of ["done", "", "First line\nSecond line"]) {
433
+ it.effect(`finishes with string summary ${JSON.stringify(summary)}`, () =>
434
+ AgentExecutor.AgentExecutor.use((executor) =>
435
+ runAgentCollect({
436
+ conversationMode: false,
437
+ executor,
438
+ respond: (_call, index) =>
439
+ index === 0
440
+ ? Stream.fromIterable([
441
+ toolCall(
442
+ "complete",
443
+ `await taskComplete(${JSON.stringify(summary)})`,
444
+ ),
445
+ ])
446
+ : Stream.die("completion must not require another model call"),
447
+ }).pipe(
448
+ Effect.map(({ exit, calls }) => {
449
+ assert.deepStrictEqual(exit, Exit.succeed(summary))
450
+ assert.strictEqual(calls.length, 1)
451
+ }),
452
+ ),
453
+ ),
454
+ )
455
+ }
456
+
457
+ for (const invocation of invalidCompletions) {
458
+ it.effect(`rejects ${invocation} without completing`, () =>
459
+ AgentExecutor.AgentExecutor.use((executor) =>
460
+ Effect.gen(function* () {
461
+ const summaries: Array<string> = []
462
+ const output = yield* executor
463
+ .execute({
464
+ script: `try { await ${invocation}; console.log("ACCEPTED") } catch (error) { console.log("REJECTED:", String(error)) } console.log("SCRIPT_SURVIVED")`,
465
+ onTaskComplete: (summary) =>
466
+ Effect.sync(() => {
467
+ summaries.push(summary)
468
+ }),
469
+ onSubagent: () => Effect.succeed(""),
470
+ onImage: () => Effect.void,
471
+ })
472
+ .pipe(Stream.mkString)
473
+ assert.deepStrictEqual(
474
+ summaries,
475
+ [],
476
+ "invalid input must not reach TaskCompleter",
477
+ )
478
+ assert.include(output, "REJECTED:")
479
+ assert.match(output, /string/i)
480
+ assert.include(output, "SCRIPT_SURVIVED")
481
+ assert.notInclude(output, "ACCEPTED")
482
+ }),
483
+ ),
484
+ )
485
+
486
+ it.effect(
487
+ `survives ${invocation} and completes on the next model call`,
488
+ () =>
489
+ AgentExecutor.AgentExecutor.use((executor) =>
490
+ runAgentCollect({
491
+ conversationMode: false,
492
+ executor,
493
+ respond: (_call, index) => {
494
+ switch (index) {
495
+ case 0:
496
+ return Stream.fromIterable([
497
+ toolCall("invalid", `await ${invocation}`),
498
+ ])
499
+ case 1:
500
+ return Stream.fromIterable([
501
+ toolCall("retry", 'await taskComplete("recovered")'),
502
+ ])
503
+ default:
504
+ return Stream.die("unexpected model call after completion")
505
+ }
506
+ },
507
+ }).pipe(
508
+ Effect.map(({ exit, calls }) => {
509
+ assert.deepStrictEqual(exit, Exit.succeed("recovered"))
510
+ assert.strictEqual(calls.length, 2)
511
+ const [result] = toolResults(calls[1]!.prompt)
512
+ assert.isDefined(
513
+ result,
514
+ "the model must receive the recoverable tool error",
515
+ )
516
+ assert.match(String(result!.result), /string/i)
517
+ }),
518
+ ),
519
+ ),
520
+ )
521
+ }
522
+ },
523
+ )
524
+
407
525
  // --- tests -------------------------------------------------------------------
408
526
 
409
527
  describe("Agent execute output cap", () => {
package/src/AgentTools.ts CHANGED
@@ -273,6 +273,7 @@ export const AgentTools = Toolkit.make(
273
273
  identifier: "output",
274
274
  }),
275
275
  dependencies: [TaskCompleter],
276
+ failure: Schema.String,
276
277
  }),
277
278
  )
278
279
 
@@ -722,7 +723,14 @@ export const AgentToolHandlersNoDeps = AgentToolsWithSearch.toLayer(
722
723
 
723
724
  ${prompt}`)
724
725
  }, Effect.orDie),
725
- taskComplete: Effect.fn("AgentTools.taskComplete")(function* (message) {
726
+ taskComplete: Effect.fn("AgentTools.taskComplete")(function* (
727
+ message: unknown,
728
+ ) {
729
+ if (typeof message !== "string") {
730
+ return yield* Effect.fail(
731
+ 'taskComplete requires a string argument. Pass your final output directly, for example: taskComplete("done").',
732
+ )
733
+ }
726
734
  const deferred = yield* TaskCompleter
727
735
  yield* deferred(message)
728
736
  }),