clanka 0.2.28 → 0.2.30

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/src/cli.ts CHANGED
@@ -197,12 +197,15 @@ Command.make("clanka", { provider, model, semantic, prompt }).pipe(
197
197
  Effect.provide([
198
198
  Agent.layerLocal({
199
199
  directory: process.cwd(),
200
- }),
200
+ }).pipe(
201
+ Layer.provide(
202
+ Option.match(semantic, {
203
+ onNone: () => Layer.empty,
204
+ onSome: Search,
205
+ }),
206
+ ),
207
+ ),
201
208
  Model,
202
- Option.match(semantic, {
203
- onNone: () => Layer.empty,
204
- onSome: Search,
205
- }),
206
209
  ]),
207
210
  )
208
211
  }),
@@ -0,0 +1,149 @@
1
+ await applyPatch(`*** Begin Patch
2
+ *** Update File: src/AgentTools.ts
3
+ @@
4
+ import * as ExaSearch from "./ExaSearch.ts"
5
+ import * as WebToMarkdown from "./WebToMarkdown.ts"
6
+ -import type * as HttpClient from "effect/unstable/http/HttpClient"
7
+ +import * as HttpClient from "effect/unstable/http/HttpClient"
8
+ @@
9
+ Tool.make("fetchMarkdown", {
10
+ description: "Fetch a web page and convert it to markdown.",
11
+ parameters: Schema.String.annotate({
12
+ identifier: "url",
13
+ }),
14
+ success: Schema.String,
15
+ }),
16
+ + Tool.make("fetchJson", {
17
+ + description: "Fetch a URL and parse the response body as JSON.",
18
+ + parameters: Schema.String.annotate({
19
+ + identifier: "url",
20
+ + }),
21
+ + success: Schema.Unknown,
22
+ + }),
23
+ Tool.make("sleep", {
24
+ description: "Sleep for a specified number of milliseconds",
25
+ parameters: Schema.Finite.annotate({
26
+ identifier: "ms",
27
+ }),
28
+ @@
29
+ const pathService = yield* Path.Path
30
+ const webSearch = yield* ExaSearch.ExaSearch
31
+ const fetchMarkdown = yield* WebToMarkdown.WebToMarkdown
32
+ + const httpClient = (yield* HttpClient.HttpClient).pipe(
33
+ + HttpClient.followRedirects(),
34
+ + HttpClient.filterStatusOk,
35
+ + HttpClient.retryTransient({
36
+ + times: 3,
37
+ + }),
38
+ + )
39
+ @@
40
+ fetchMarkdown: Effect.fn("AgentTools.fetchMarkdown")(function* (url) {
41
+ yield* Effect.logInfo(`Calling "fetchMarkdown"`).pipe(
42
+ Effect.annotateLogs({ url }),
43
+ )
44
+ return yield* fetchMarkdown.convertUrl(url)
45
+ }, Effect.orDie),
46
+ + fetchJson: Effect.fn("AgentTools.fetchJson")(function* (url) {
47
+ + yield* Effect.logInfo(`Calling "fetchJson"`).pipe(
48
+ + Effect.annotateLogs({ url }),
49
+ + )
50
+ + const response = yield* httpClient.get(url)
51
+ + return yield* response.json
52
+ + }, Effect.orDie),
53
+ sleep: Effect.fn("AgentTools.sleep")(function* (ms) {
54
+ yield* Effect.logInfo(`Calling "sleep" for ${ms}ms`)
55
+ return yield* Effect.sleep(ms)
56
+ }),
57
+ *** Add File: src/AgentTools.test.ts
58
+ +import * as NodeServices from "@effect/platform-node/NodeServices"
59
+ +import { assert, describe, it } from "@effect/vitest"
60
+ +import * as Effect from "effect/Effect"
61
+ +import * as Stream from "effect/Stream"
62
+ +import * as Ref from "effect/Ref"
63
+ +import {
64
+ + HttpClient,
65
+ + type HttpClientRequest,
66
+ + HttpClientResponse,
67
+ +} from "effect/unstable/http"
68
+ +import * as AgentTools from "./AgentTools.ts"
69
+ +import * as ToolkitRenderer from "./ToolkitRenderer.ts"
70
+ +
71
+ +describe("AgentTools", () => {
72
+ + it.effect("renders fetchJson in the toolkit dts", () =>
73
+ + Effect.gen(function* () {
74
+ + const renderer = yield* ToolkitRenderer.ToolkitRenderer
75
+ + const output = renderer.render(AgentTools.AgentTools)
76
+ +
77
+ + assert.include(
78
+ + output,
79
+ + 'declare function fetchJson(url: string): Promise<unknown>',
80
+ + )
81
+ + }).pipe(Effect.provide(ToolkitRenderer.ToolkitRenderer.layer)),
82
+ + )
83
+ +
84
+ + it.effect("fetchJson follows redirects and parses json", () =>
85
+ + Effect.gen(function* () {
86
+ + const requests = yield* Ref.make<Array<string>>([])
87
+ + const client = HttpClient.make(
88
+ + (request: HttpClientRequest.HttpClientRequest) =>
89
+ + Effect.gen(function* () {
90
+ + yield* Ref.update(requests, (current) => [...current, request.url])
91
+ +
92
+ + if (request.url === "https://example.com/start") {
93
+ + return HttpClientResponse.fromWeb(
94
+ + request,
95
+ + new Response(null, {
96
+ + status: 302,
97
+ + headers: { location: "/data" },
98
+ + }),
99
+ + )
100
+ + }
101
+ +
102
+ + if (request.url === "https://example.com/data") {
103
+ + return HttpClientResponse.fromWeb(
104
+ + request,
105
+ + new Response('{"message":"hello","count":1}', {
106
+ + status: 200,
107
+ + headers: { "content-type": "application/json" },
108
+ + }),
109
+ + )
110
+ + }
111
+ +
112
+ + return HttpClientResponse.fromWeb(
113
+ + request,
114
+ + new Response("not found", { status: 404 }),
115
+ + )
116
+ + }),
117
+ + )
118
+ +
119
+ + const toolkit = yield* AgentTools.AgentTools.asEffect()
120
+ + const stream = yield* toolkit.handle(
121
+ + "fetchJson",
122
+ + "https://example.com/start",
123
+ + )
124
+ + const results = Array.from(yield* Stream.runCollect(stream))
125
+ + const seen = yield* Ref.get(requests)
126
+ +
127
+ + assert.deepStrictEqual(seen, [
128
+ + "https://example.com/start",
129
+ + "https://example.com/data",
130
+ + ])
131
+ + assert.deepStrictEqual(results[0]?.result, {
132
+ + message: "hello",
133
+ + count: 1,
134
+ + })
135
+ + }).pipe(
136
+ + Effect.provide(AgentTools.AgentToolHandlersTest),
137
+ + Effect.provide(NodeServices.layer),
138
+ + Effect.provideService(HttpClient.HttpClient, client),
139
+ + ),
140
+ + )
141
+ +})
142
+ *** Add File: .changeset/warm-poets-share.md
143
+ +---
144
+ +"clanka": patch
145
+ +---
146
+ +
147
+ +Add a built-in `fetchJson` agent tool for fetching and parsing JSON responses.
148
+ *** End Patch`);
149
+ console.log('patch applied');
@@ -0,0 +1,149 @@
1
+ await applyPatch(`*** Begin Patch
2
+ *** Update File: src/AgentTools.ts
3
+ @@
4
+ import * as ExaSearch from "./ExaSearch.ts"
5
+ import * as WebToMarkdown from "./WebToMarkdown.ts"
6
+ -import type * as HttpClient from "effect/unstable/http/HttpClient"
7
+ +import * as HttpClient from "effect/unstable/http/HttpClient"
8
+ @@
9
+ Tool.make("fetchMarkdown", {
10
+ description: "Fetch a web page and convert it to markdown.",
11
+ parameters: Schema.String.annotate({
12
+ identifier: "url",
13
+ }),
14
+ success: Schema.String,
15
+ }),
16
+ + Tool.make("fetchJson", {
17
+ + description: "Fetch a URL and parse the response body as JSON.",
18
+ + parameters: Schema.String.annotate({
19
+ + identifier: "url",
20
+ + }),
21
+ + success: Schema.Unknown,
22
+ + }),
23
+ Tool.make("sleep", {
24
+ description: "Sleep for a specified number of milliseconds",
25
+ parameters: Schema.Finite.annotate({
26
+ identifier: "ms",
27
+ }),
28
+ @@
29
+ const pathService = yield* Path.Path
30
+ const webSearch = yield* ExaSearch.ExaSearch
31
+ const fetchMarkdown = yield* WebToMarkdown.WebToMarkdown
32
+ + const httpClient = (yield* HttpClient.HttpClient).pipe(
33
+ + HttpClient.followRedirects(),
34
+ + HttpClient.filterStatusOk,
35
+ + HttpClient.retryTransient({
36
+ + times: 3,
37
+ + }),
38
+ + )
39
+ @@
40
+ fetchMarkdown: Effect.fn("AgentTools.fetchMarkdown")(function* (url) {
41
+ yield* Effect.logInfo(\`Calling "fetchMarkdown"\`).pipe(
42
+ Effect.annotateLogs({ url }),
43
+ )
44
+ return yield* fetchMarkdown.convertUrl(url)
45
+ }, Effect.orDie),
46
+ + fetchJson: Effect.fn("AgentTools.fetchJson")(function* (url) {
47
+ + yield* Effect.logInfo(\`Calling "fetchJson"\`).pipe(
48
+ + Effect.annotateLogs({ url }),
49
+ + )
50
+ + const response = yield* httpClient.get(url)
51
+ + return yield* response.json
52
+ + }, Effect.orDie),
53
+ sleep: Effect.fn("AgentTools.sleep")(function* (ms) {
54
+ yield* Effect.logInfo(\`Calling "sleep" for \${ms}ms\`)
55
+ return yield* Effect.sleep(ms)
56
+ }),
57
+ *** Add File: src/AgentTools.test.ts
58
+ +import * as NodeServices from "@effect/platform-node/NodeServices"
59
+ +import { assert, describe, it } from "@effect/vitest"
60
+ +import * as Effect from "effect/Effect"
61
+ +import * as Stream from "effect/Stream"
62
+ +import * as Ref from "effect/Ref"
63
+ +import {
64
+ + HttpClient,
65
+ + type HttpClientRequest,
66
+ + HttpClientResponse,
67
+ +} from "effect/unstable/http"
68
+ +import * as AgentTools from "./AgentTools.ts"
69
+ +import * as ToolkitRenderer from "./ToolkitRenderer.ts"
70
+ +
71
+ +describe("AgentTools", () => {
72
+ + it.effect("renders fetchJson in the toolkit dts", () =>
73
+ + Effect.gen(function* () {
74
+ + const renderer = yield* ToolkitRenderer.ToolkitRenderer
75
+ + const output = renderer.render(AgentTools.AgentTools)
76
+ +
77
+ + assert.include(
78
+ + output,
79
+ + 'declare function fetchJson(url: string): Promise<unknown>',
80
+ + )
81
+ + }).pipe(Effect.provide(ToolkitRenderer.ToolkitRenderer.layer)),
82
+ + )
83
+ +
84
+ + it.effect("fetchJson follows redirects and parses json", () =>
85
+ + Effect.gen(function* () {
86
+ + const requests = yield* Ref.make<Array<string>>([])
87
+ + const client = HttpClient.make(
88
+ + (request: HttpClientRequest.HttpClientRequest) =>
89
+ + Effect.gen(function* () {
90
+ + yield* Ref.update(requests, (current) => [...current, request.url])
91
+ +
92
+ + if (request.url === "https://example.com/start") {
93
+ + return HttpClientResponse.fromWeb(
94
+ + request,
95
+ + new Response(null, {
96
+ + status: 302,
97
+ + headers: { location: "/data" },
98
+ + }),
99
+ + )
100
+ + }
101
+ +
102
+ + if (request.url === "https://example.com/data") {
103
+ + return HttpClientResponse.fromWeb(
104
+ + request,
105
+ + new Response('{"message":"hello","count":1}', {
106
+ + status: 200,
107
+ + headers: { "content-type": "application/json" },
108
+ + }),
109
+ + )
110
+ + }
111
+ +
112
+ + return HttpClientResponse.fromWeb(
113
+ + request,
114
+ + new Response("not found", { status: 404 }),
115
+ + )
116
+ + }),
117
+ + )
118
+ +
119
+ + const toolkit = yield* AgentTools.AgentTools.asEffect()
120
+ + const stream = yield* toolkit.handle(
121
+ + "fetchJson",
122
+ + "https://example.com/start",
123
+ + )
124
+ + const results = Array.from(yield* Stream.runCollect(stream))
125
+ + const seen = yield* Ref.get(requests)
126
+ +
127
+ + assert.deepStrictEqual(seen, [
128
+ + "https://example.com/start",
129
+ + "https://example.com/data",
130
+ + ])
131
+ + assert.deepStrictEqual(results[0]?.result, {
132
+ + message: "hello",
133
+ + count: 1,
134
+ + })
135
+ + }).pipe(
136
+ + Effect.provide(AgentTools.AgentToolHandlersTest),
137
+ + Effect.provide(NodeServices.layer),
138
+ + Effect.provideService(HttpClient.HttpClient, client),
139
+ + ),
140
+ + )
141
+ +})
142
+ *** Add File: .changeset/warm-poets-share.md
143
+ +---
144
+ +"clanka": patch
145
+ +---
146
+ +
147
+ +Add a built-in \`fetchJson\` agent tool for fetching and parsing JSON responses.
148
+ *** End Patch`);
149
+ console.log('patch applied');
@@ -0,0 +1,68 @@
1
+ const patch = `*** Begin Patch
2
+ *** Update File: packages/effect/src/Stream.ts
3
+ @@
4
+ - * Maps each element to a stream and concatenates the results in order.
5
+ + * Maps each element to a stream and concatenates the results in order.
6
+ + *
7
+ + * Set \`switch: true\` to interrupt the previous inner stream when a new
8
+ + * element arrives.
9
+ @@
10
+ <A, A2, E2, R2>(
11
+ f: (a: A) => Stream<A2, E2, R2>,
12
+ options?: {
13
+ readonly concurrency?: number | \"unbounded\" | undefined
14
+ readonly bufferSize?: number | undefined
15
+ + readonly switch?: boolean | undefined
16
+ } | undefined
17
+ ): <E, R>(self: Stream<A, E, R>) => Stream<A2, E2 | E, R2 | R>
18
+ <A, E, R, A2, E2, R2>(
19
+ self: Stream<A, E, R>,
20
+ f: (a: A) => Stream<A2, E2, R2>,
21
+ options?: {
22
+ readonly concurrency?: number | \"unbounded\" | undefined
23
+ readonly bufferSize?: number | undefined
24
+ + readonly switch?: boolean | undefined
25
+ } | undefined
26
+ ): Stream<A2, E | E2, R | R2>
27
+ } = dual((args) => isStream(args[0]), <A, E, R, A2, E2, R2>(
28
+ self: Stream<A, E, R>,
29
+ f: (a: A) => Stream<A2, E2, R2>,
30
+ options?: {
31
+ readonly concurrency?: number | \"unbounded\" | undefined
32
+ readonly bufferSize?: number | undefined
33
+ + readonly switch?: boolean | undefined
34
+ } | undefined
35
+ ): Stream<A2, E | E2, R | R2> =>
36
+ self.channel.pipe(
37
+ Channel.flattenArray,
38
+ - Channel.flatMap((a) => f(a).channel, options),
39
+ + (options?.switch ? Channel.switchMap : Channel.flatMap)((a) => f(a).channel, options),
40
+ fromChannel
41
+ ))
42
+ *** Update File: packages/effect/test/Stream.test.ts
43
+ @@
44
+ describe("callback", () => {
45
+ @@
46
+ }))
47
+ })
48
+ +
49
+ + describe("flatMap", () => {
50
+ + it.effect("supports switch semantics", () =>
51
+ + Effect.gen(function*() {
52
+ + const result = yield* Stream.make(1, 2, 3).pipe(
53
+ + Stream.flatMap((n) => n === 3 ? Stream.make(n) : Stream.never, { switch: true }),
54
+ + Stream.runCollect
55
+ + )
56
+ + assert.deepStrictEqual(result, [3])
57
+ + }))
58
+ + })
59
+
60
+ describe("bufferArray", () => {
61
+ *** Add File: .changeset/olive-bears-kiss.md
62
+ +---
63
+ +"effect": patch
64
+ +---
65
+ +
66
+ +Add a `switch` option to `Stream.flatMap` for `switchMap` semantics.
67
+ *** End Patch`
68
+ console.log(await applyPatch(patch))
@@ -0,0 +1,68 @@
1
+ const patch = `*** Begin Patch
2
+ *** Update File: packages/effect/src/Stream.ts
3
+ @@
4
+ - * Maps each element to a stream and concatenates the results in order.
5
+ + * Maps each element to a stream and concatenates the results in order.
6
+ + *
7
+ + * Set \`switch: true\` to interrupt the previous inner stream when a new
8
+ + * element arrives.
9
+ @@
10
+ <A, A2, E2, R2>(
11
+ f: (a: A) => Stream<A2, E2, R2>,
12
+ options?: {
13
+ readonly concurrency?: number | "unbounded" | undefined
14
+ readonly bufferSize?: number | undefined
15
+ + readonly switch?: boolean | undefined
16
+ } | undefined
17
+ ): <E, R>(self: Stream<A, E, R>) => Stream<A2, E2 | E, R2 | R>
18
+ <A, E, R, A2, E2, R2>(
19
+ self: Stream<A, E, R>,
20
+ f: (a: A) => Stream<A2, E2, R2>,
21
+ options?: {
22
+ readonly concurrency?: number | "unbounded" | undefined
23
+ readonly bufferSize?: number | undefined
24
+ + readonly switch?: boolean | undefined
25
+ } | undefined
26
+ ): Stream<A2, E | E2, R | R2>
27
+ } = dual((args) => isStream(args[0]), <A, E, R, A2, E2, R2>(
28
+ self: Stream<A, E, R>,
29
+ f: (a: A) => Stream<A2, E2, R2>,
30
+ options?: {
31
+ readonly concurrency?: number | "unbounded" | undefined
32
+ readonly bufferSize?: number | undefined
33
+ + readonly switch?: boolean | undefined
34
+ } | undefined
35
+ ): Stream<A2, E | E2, R | R2> =>
36
+ self.channel.pipe(
37
+ Channel.flattenArray,
38
+ - Channel.flatMap((a) => f(a).channel, options),
39
+ + (options?.switch ? Channel.switchMap : Channel.flatMap)((a) => f(a).channel, options),
40
+ fromChannel
41
+ ))
42
+ *** Update File: packages/effect/test/Stream.test.ts
43
+ @@
44
+ describe("callback", () => {
45
+ @@
46
+ }))
47
+ })
48
+ +
49
+ + describe("flatMap", () => {
50
+ + it.effect("supports switch semantics", () =>
51
+ + Effect.gen(function*() {
52
+ + const result = yield* Stream.make(1, 2, 3).pipe(
53
+ + Stream.flatMap((n) => n === 3 ? Stream.make(n) : Stream.never, { switch: true }),
54
+ + Stream.runCollect
55
+ + )
56
+ + assert.deepStrictEqual(result, [3])
57
+ + }))
58
+ + })
59
+
60
+ describe("bufferArray", () => {
61
+ *** Add File: .changeset/olive-bears-kiss.md
62
+ +---
63
+ +"effect": patch
64
+ +---
65
+ +
66
+ +Add a \`switch\` option to \`Stream.flatMap\` for \`switchMap\` semantics.
67
+ *** End Patch`
68
+ console.log(await applyPatch(patch))