@hypen-space/gloop-effect 0.1.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/README.md +422 -0
- package/dist/AIProvider.d.ts +43 -0
- package/dist/AIProvider.d.ts.map +1 -0
- package/dist/AIProvider.js +27 -0
- package/dist/AIProvider.js.map +1 -0
- package/dist/Agent.d.ts +91 -0
- package/dist/Agent.d.ts.map +1 -0
- package/dist/Agent.js +378 -0
- package/dist/Agent.js.map +1 -0
- package/dist/Conversation.d.ts +44 -0
- package/dist/Conversation.d.ts.map +1 -0
- package/dist/Conversation.js +124 -0
- package/dist/Conversation.js.map +1 -0
- package/dist/Errors.d.ts +102 -0
- package/dist/Errors.d.ts.map +1 -0
- package/dist/Errors.js +80 -0
- package/dist/Errors.js.map +1 -0
- package/dist/Interpreter.d.ts +62 -0
- package/dist/Interpreter.d.ts.map +1 -0
- package/dist/Interpreter.js +217 -0
- package/dist/Interpreter.js.map +1 -0
- package/dist/Schema.d.ts +188 -0
- package/dist/Schema.d.ts.map +1 -0
- package/dist/Schema.js +135 -0
- package/dist/Schema.js.map +1 -0
- package/dist/Tool.d.ts +70 -0
- package/dist/Tool.d.ts.map +1 -0
- package/dist/Tool.js +138 -0
- package/dist/Tool.js.map +1 -0
- package/dist/defaults/Builtins.d.ts +23 -0
- package/dist/defaults/Builtins.d.ts.map +1 -0
- package/dist/defaults/Builtins.js +38 -0
- package/dist/defaults/Builtins.js.map +1 -0
- package/dist/defaults/FileMemory.d.ts +16 -0
- package/dist/defaults/FileMemory.d.ts.map +1 -0
- package/dist/defaults/FileMemory.js +32 -0
- package/dist/defaults/FileMemory.js.map +1 -0
- package/dist/defaults/OpenRouter.d.ts +20 -0
- package/dist/defaults/OpenRouter.d.ts.map +1 -0
- package/dist/defaults/OpenRouter.js +68 -0
- package/dist/defaults/OpenRouter.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
# @hypen-space/gloop-effect
|
|
2
|
+
|
|
3
|
+
Effect-TS native agent loop. Pairs with [`@hypen-space/gloop-loop`](../gloop-loop) — the Form ADT, slash-command parser, skill helpers, and builtin tool bodies are shared; the actor shell, provider interface, error model, and event bus are rebuilt on Effect primitives (`Stream`, `Fiber`, `PubSub`, `Ref`, `Queue`).
|
|
4
|
+
|
|
5
|
+
## Why use this over `gloop-loop`?
|
|
6
|
+
|
|
7
|
+
- **Typed errors** — every failure is a `Schema.TaggedError`, so `catchTag`/`catchTags` narrows precisely.
|
|
8
|
+
- **Stream events** — `agent.events: Stream<AgentEvent>` fans out via `PubSub`; subscribers get backpressure + filter/map/merge for free.
|
|
9
|
+
- **Fiber interrupts** — `agent.interrupt` uses structured concurrency, not an `AbortController`.
|
|
10
|
+
- **Layer-based DI** — providers, memory, and IO drop in as `Layer`s at the app root.
|
|
11
|
+
- **Spans everywhere** — public methods wrap themselves in `Effect.fn(...)` / `Effect.withSpan(...)` with useful attributes. Plug in `@effect/opentelemetry` and get a full trace per turn.
|
|
12
|
+
- **Schema-unified** — events, IDs, messages, tool calls are all `Schema.TaggedStruct` — JSON-codec-ready for RPC transport.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bun add @hypen-space/gloop-effect effect
|
|
18
|
+
# or: npm install / pnpm add
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
You need an `OPENROUTER_API_KEY` in the environment for the default provider.
|
|
22
|
+
|
|
23
|
+
## Quick start — a deploy bot with 3 tools
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { Effect, Option, Stream } from "effect"
|
|
27
|
+
import { NodeRuntime } from "@effect/platform-node"
|
|
28
|
+
import {
|
|
29
|
+
Agent,
|
|
30
|
+
OpenRouterProviderLive,
|
|
31
|
+
type Tool,
|
|
32
|
+
} from "@hypen-space/gloop-effect"
|
|
33
|
+
|
|
34
|
+
const listEnvs: Tool<never> = {
|
|
35
|
+
name: "ListEnvironments",
|
|
36
|
+
description: "List all deployment environments.",
|
|
37
|
+
arguments: [],
|
|
38
|
+
execute: () => Effect.succeed("staging, prod, canary"),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const getStatus: Tool<never> = {
|
|
42
|
+
name: "GetStatus",
|
|
43
|
+
description: "Get the current deployment status of an environment.",
|
|
44
|
+
arguments: [{ name: "env", description: "Environment name" }],
|
|
45
|
+
execute: (args) => Effect.succeed(`${args.env}: healthy, 3 instances`),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const deploy: Tool<never> = {
|
|
49
|
+
name: "Deploy",
|
|
50
|
+
description: "Deploy the current build to an environment.",
|
|
51
|
+
arguments: [
|
|
52
|
+
{ name: "env", description: "Target environment" },
|
|
53
|
+
{ name: "version", description: "Version tag" },
|
|
54
|
+
],
|
|
55
|
+
// Returning Some(reason) pauses the turn for ConfirmRequest.
|
|
56
|
+
askPermission: (args) =>
|
|
57
|
+
args.env === "prod"
|
|
58
|
+
? Option.some(`Deploy ${args.version} to prod?`)
|
|
59
|
+
: Option.none(),
|
|
60
|
+
execute: (args) => Effect.succeed(`Deployed ${args.version} to ${args.env}`),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const program = Effect.gen(function* () {
|
|
64
|
+
const agent = yield* Agent.make({
|
|
65
|
+
model: "anthropic/claude-sonnet-4.5",
|
|
66
|
+
system: "You are a deploy bot. Use the tools to help the user.",
|
|
67
|
+
tools: [listEnvs, getStatus, deploy],
|
|
68
|
+
// Auto-approve every confirm. For a TUI, omit this and listen for
|
|
69
|
+
// ConfirmRequest events instead.
|
|
70
|
+
confirm: () => Effect.succeed(true),
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// Render streaming chunks to stdout.
|
|
74
|
+
yield* Effect.forkScoped(
|
|
75
|
+
agent.events.pipe(
|
|
76
|
+
Stream.runForEach((e) =>
|
|
77
|
+
e._tag === "StreamChunk"
|
|
78
|
+
? Effect.sync(() => process.stdout.write(e.text))
|
|
79
|
+
: Effect.void,
|
|
80
|
+
),
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
yield* agent.sendSync("deploy v2.1.0 to staging and report status")
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
NodeRuntime.runMain(
|
|
88
|
+
Effect.scoped(program).pipe(
|
|
89
|
+
Effect.provide(
|
|
90
|
+
OpenRouterProviderLive({ apiKey: process.env.OPENROUTER_API_KEY! }),
|
|
91
|
+
),
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## The shape of `Agent`
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
interface Agent {
|
|
100
|
+
send: (msg: AgentMessage | string) => Effect<MessageId>
|
|
101
|
+
sendSync: (msg: AgentMessage | string) => Effect<void, AgentError>
|
|
102
|
+
events: Stream<AgentEvent>
|
|
103
|
+
eventsOf: <T extends AgentEvent["_tag"]>(tag: T) => Stream<AgentEventOf<T>>
|
|
104
|
+
interrupt: Effect<void>
|
|
105
|
+
stop: Effect<void>
|
|
106
|
+
awaitIdle: Effect<void>
|
|
107
|
+
pending: Effect<number>
|
|
108
|
+
|
|
109
|
+
addTool: <E extends AgentError>(tool: Tool<E>) => Effect<void>
|
|
110
|
+
removeTool: (name: string) => Effect<void>
|
|
111
|
+
setTools: (tools: ReadonlyArray<AnyTool>) => Effect<void>
|
|
112
|
+
setSystem: (prompt: string) => Effect<void>
|
|
113
|
+
clear: Effect<void>
|
|
114
|
+
|
|
115
|
+
respondToConfirm: (id: RequestId, ok: boolean) => Effect<void>
|
|
116
|
+
respondToAsk: (id: RequestId, answer: string) => Effect<void>
|
|
117
|
+
|
|
118
|
+
registry: ToolRegistry
|
|
119
|
+
conversation: ConversationHandle
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`Agent.make` is **scoped** — run inside `Effect.scoped` (or provide a `Scope` layer) so resources tear down cleanly.
|
|
124
|
+
|
|
125
|
+
## Subscribing to events
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
// Full firehose
|
|
129
|
+
agent.events.pipe(Stream.runForEach(handleEvent))
|
|
130
|
+
|
|
131
|
+
// Single tag — type-narrowed
|
|
132
|
+
agent.eventsOf("ToolDone").pipe(
|
|
133
|
+
Stream.runForEach((e) => Effect.log(`tool ${e.name} → ${e.ok}`)),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
// Merged or filtered
|
|
137
|
+
Stream.merge(
|
|
138
|
+
agent.eventsOf("StreamChunk"),
|
|
139
|
+
agent.eventsOf("TaskComplete"),
|
|
140
|
+
).pipe(Stream.runForEach(...))
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Every call creates a fresh subscription via `PubSub.subscribe` — late subscribers miss past events. Subscribe *before* sending if you need a specific event (or use `sendSync`, which subscribes internally and awaits the matching `TurnEnd`).
|
|
144
|
+
|
|
145
|
+
### Event variants
|
|
146
|
+
|
|
147
|
+
`AgentEvent` is a `Schema.Union` of 17 `TaggedStruct` variants, discriminated on `_tag`:
|
|
148
|
+
|
|
149
|
+
| Tag | Payload | When |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| `TurnStart` | `message` | A message is about to be processed |
|
|
152
|
+
| `TurnEnd` | — | The current turn finished |
|
|
153
|
+
| `Busy` / `Idle` | — | The loop picked up / drained work |
|
|
154
|
+
| `QueueChanged` | `pending` | Inbox size changed |
|
|
155
|
+
| `StreamChunk` | `text` | Streamed assistant text delta |
|
|
156
|
+
| `StreamDone` | — | Stream finished (tool calls may follow) |
|
|
157
|
+
| `ToolStart` / `ToolDone` | `id`, `name`, … | Tool invocation lifecycle |
|
|
158
|
+
| `Memory` | `op`, `content` | Agent called Remember / Forget |
|
|
159
|
+
| `SystemRefreshed` | — | System prompt was rebuilt |
|
|
160
|
+
| `TaskComplete` | `summary` | CompleteTask was called |
|
|
161
|
+
| `Interrupted` | — | Current turn was aborted |
|
|
162
|
+
| `Error` / `Fatal` | `error: AgentError` | Non-fatal / fatal turn error |
|
|
163
|
+
| `ConfirmRequest` / `AskRequest` | `id`, … | Blocking user prompt |
|
|
164
|
+
|
|
165
|
+
## Custom tools
|
|
166
|
+
|
|
167
|
+
Tools are plain objects whose `execute` returns an `Effect`. The error channel is constrained to `AgentError` so failures fit the interpreter's union:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import { Effect, Option } from "effect"
|
|
171
|
+
import { ToolExecutionError, type Tool } from "@hypen-space/gloop-effect"
|
|
172
|
+
|
|
173
|
+
const fetchUrl: Tool<ToolExecutionError> = {
|
|
174
|
+
name: "FetchUrl",
|
|
175
|
+
description: "HTTP GET a URL and return the body",
|
|
176
|
+
arguments: [{ name: "url", description: "Full URL" }],
|
|
177
|
+
askPermission: (args) => Option.none(), // None → run immediately
|
|
178
|
+
execute: (args) =>
|
|
179
|
+
Effect.tryPromise({
|
|
180
|
+
try: () => fetch(args.url!).then((r) => r.text()),
|
|
181
|
+
catch: (e) =>
|
|
182
|
+
new ToolExecutionError({
|
|
183
|
+
name: "FetchUrl",
|
|
184
|
+
message: e instanceof Error ? e.message : String(e),
|
|
185
|
+
cause: e,
|
|
186
|
+
}),
|
|
187
|
+
}),
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Tool failures fold into a `ToolResult { success: false }` — the model sees the error and decides whether to retry. If you want a tool error to be **turn-fatal**, return `Effect.die(...)` instead of `Effect.fail(...)`.
|
|
192
|
+
|
|
193
|
+
### Builtins
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { primitiveTools } from "@hypen-space/gloop-effect"
|
|
197
|
+
|
|
198
|
+
const agent = yield* Agent.make({
|
|
199
|
+
model: "...",
|
|
200
|
+
system: "...",
|
|
201
|
+
tools: primitiveTools(), // ReadFile, WriteFile, Patch_file, Bash, CompleteTask, AskUser, Remember, Forget, ManageContext
|
|
202
|
+
})
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Wraps `gloop-loop`'s builtins as `Tool<ToolExecutionError>`. Pass a custom `BuiltinIO` to override filesystem / shell semantics.
|
|
206
|
+
|
|
207
|
+
## Skills (Agent Skills / `SKILL.md`)
|
|
208
|
+
|
|
209
|
+
Pass discovered skills via `AgentMakeOptions.skills` — the agent:
|
|
210
|
+
|
|
211
|
+
1. **Merges names + descriptions into the system prompt** so the model knows what exists.
|
|
212
|
+
2. **Auto-registers `InvokeSkill`** as a tool so the model can call `InvokeSkill(name, arguments)` and receive the fully-substituted skill body as the next turn's input. (If you pass your own tool named `InvokeSkill`, it takes precedence.)
|
|
213
|
+
3. **Resolves slash commands** in user messages: `/skills` lists, `/skill <name> [args]` runs, `/<name> [args]` runs if `<name>` matches a skill.
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import {
|
|
217
|
+
Agent,
|
|
218
|
+
parseSkillMarkdown,
|
|
219
|
+
type Skill,
|
|
220
|
+
} from "@hypen-space/gloop-effect"
|
|
221
|
+
import { readdir, readFile } from "node:fs/promises"
|
|
222
|
+
|
|
223
|
+
// Your host discovers SKILL.md files wherever — .claude/skills, .agent/skills, etc.
|
|
224
|
+
const skills: Skill[] = await Promise.all(
|
|
225
|
+
(await readdir(".claude/skills")).map(async (dir) => {
|
|
226
|
+
const body = await readFile(`.claude/skills/${dir}/SKILL.md`, "utf8")
|
|
227
|
+
return parseSkillMarkdown(body, dir)
|
|
228
|
+
}),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
const agent = yield* Agent.make({
|
|
232
|
+
model: "...",
|
|
233
|
+
system: "You are a designer.",
|
|
234
|
+
skills,
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
yield* agent.sendSync("/skill web-design-guidelines review the homepage")
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Skill discovery lives in **your host code** — the library doesn't read the filesystem for them. Use `parseSkillMarkdown` / `findSkill` / `mergeSkillsIntoSystem` / `formatSkillsListing` / `applySkillSubstitutions` / `splitSkillArguments` / `matchSkillSlash` / `skillInvocationToThinkInput` / `thinkInputFromSkillSubcommand` — all re-exported from this package.
|
|
241
|
+
|
|
242
|
+
## Hosts, memory, and defaults
|
|
243
|
+
|
|
244
|
+
Three shipped defaults — each a single import away:
|
|
245
|
+
|
|
246
|
+
### `OpenRouterProviderLive`
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
import { OpenRouterProviderLive } from "@hypen-space/gloop-effect"
|
|
250
|
+
|
|
251
|
+
const ProviderLive = OpenRouterProviderLive({
|
|
252
|
+
apiKey: process.env.OPENROUTER_API_KEY!,
|
|
253
|
+
httpReferer: "https://myapp.example", // optional
|
|
254
|
+
xTitle: "my-app", // optional
|
|
255
|
+
})
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Provides the `AIProvider` service. Spans on `OpenRouterProvider.complete` and `.stream.*` with `model` attributes.
|
|
259
|
+
|
|
260
|
+
### `fileMemory`
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
import { fileMemory } from "@hypen-space/gloop-effect"
|
|
264
|
+
|
|
265
|
+
const memory = fileMemory({
|
|
266
|
+
path: "./.gloop/memory.md", // default
|
|
267
|
+
maxEntryLength: 500, // default
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
const agent = yield* Agent.make({
|
|
271
|
+
model: "...",
|
|
272
|
+
system: "...",
|
|
273
|
+
remember: memory.remember, // plug into hooks
|
|
274
|
+
forget: memory.forget,
|
|
275
|
+
})
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### `createNodeIO`
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
import { createNodeIO, primitiveTools } from "@hypen-space/gloop-effect"
|
|
282
|
+
|
|
283
|
+
const io = createNodeIO()
|
|
284
|
+
const tools = primitiveTools(io) // ReadFile/WriteFile/Bash use these
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Tracing
|
|
288
|
+
|
|
289
|
+
Every public method is a named span:
|
|
290
|
+
|
|
291
|
+
| Span | Attributes |
|
|
292
|
+
|---|---|
|
|
293
|
+
| `Agent.send` / `sendSync` / `interrupt` / `stop` / `awaitIdle` / `respondTo*` | `messageId`, `requestId` |
|
|
294
|
+
| `Agent.runTurn` | `messageId`, `role`, `contentLength` |
|
|
295
|
+
| `Conversation.send` / `.stream` | `model`, `historyLength`, `toolCount` |
|
|
296
|
+
| `OpenRouterProvider.complete` / `.stream.*` | `model` |
|
|
297
|
+
| `Interpreter.evalForm` | `form` (tag) |
|
|
298
|
+
| `Interpreter.evalThink` | — |
|
|
299
|
+
| `Interpreter.evalInvoke` | `toolCount` |
|
|
300
|
+
| `Interpreter.dispatchCall` | `tool`, `kind`, `success`, `denied` |
|
|
301
|
+
| `ToolRegistry.register` / `.unregister` | `toolName` |
|
|
302
|
+
|
|
303
|
+
Wire an exporter with `@effect/opentelemetry`:
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
import { NodeSdk } from "@effect/opentelemetry"
|
|
307
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"
|
|
308
|
+
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base"
|
|
309
|
+
|
|
310
|
+
const TracingLive = NodeSdk.layer(() => ({
|
|
311
|
+
resource: { serviceName: "my-agent" },
|
|
312
|
+
spanProcessor: new BatchSpanProcessor(
|
|
313
|
+
new OTLPTraceExporter({ url: "http://localhost:4318/v1/traces" }),
|
|
314
|
+
),
|
|
315
|
+
}))
|
|
316
|
+
|
|
317
|
+
NodeRuntime.runMain(
|
|
318
|
+
Effect.scoped(program).pipe(
|
|
319
|
+
Effect.provide(Layer.mergeAll(ProviderLive, TracingLive)),
|
|
320
|
+
),
|
|
321
|
+
)
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
No code change in your app — the spans already exist.
|
|
325
|
+
|
|
326
|
+
## Logging
|
|
327
|
+
|
|
328
|
+
Two channels:
|
|
329
|
+
|
|
330
|
+
**Effect-native** — `Effect.log` / `Effect.logInfo` / `Effect.logDebug`. Provide any `Logger` layer at the root:
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
import { Logger, LogLevel } from "effect"
|
|
334
|
+
|
|
335
|
+
Effect.provide(Logger.pretty), // human-readable
|
|
336
|
+
Effect.provide(Logger.json), // one JSON line per log
|
|
337
|
+
Effect.provide(Logger.withMinimumLogLevel(LogLevel.Debug))
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Internally, `LLM_INPUT` / `LLM_OUTPUT` / `TOOL_CALLS` are emitted at `Debug` level.
|
|
341
|
+
|
|
342
|
+
**Host debug hook** — `AgentMakeOptions.log: (label, content) => Effect<void>`. Fires alongside the Effect logger. Useful for piping transcripts to a file or a TUI panel:
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
const agent = yield* Agent.make({
|
|
346
|
+
model: "...",
|
|
347
|
+
system: "...",
|
|
348
|
+
log: (label, content) =>
|
|
349
|
+
Effect.sync(() => fs.appendFileSync("debug.log", `[${label}] ${content}\n`)),
|
|
350
|
+
})
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Errors
|
|
354
|
+
|
|
355
|
+
All failures are `Schema.TaggedError` with a `message` field:
|
|
356
|
+
|
|
357
|
+
| Error | When |
|
|
358
|
+
|---|---|
|
|
359
|
+
| `AIProviderError` | Provider call failed — carries `op`, `model`, `provider`, `cause` |
|
|
360
|
+
| `ToolNotFoundError` | Model called a tool that isn't registered |
|
|
361
|
+
| `ToolExecutionError` | A tool's `execute` failed |
|
|
362
|
+
| `ToolPermissionDeniedError` | A gated tool was denied by the host |
|
|
363
|
+
| `AgentInterruptedError` | The current turn was interrupted |
|
|
364
|
+
| `FatalAgentError` | Turn-level error classified as fatal via `isFatal` |
|
|
365
|
+
| `FileIOError` | `BuiltinIO` read/write/delete failed — carries `op`, `path` |
|
|
366
|
+
| `ShellExecError` | `BuiltinIO.exec` non-zero exit |
|
|
367
|
+
| `MemoryError` | `fileMemory` read/write failed |
|
|
368
|
+
|
|
369
|
+
`AgentError` is the union. Use `catchTag`/`catchTags` — never `catchAll`:
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
yield* agent.sendSync(userInput).pipe(
|
|
373
|
+
Effect.catchTags({
|
|
374
|
+
AIProviderError: (e) => showBanner(`Provider down: ${e.provider ?? "?"}`),
|
|
375
|
+
AgentInterruptedError: () => showBanner("Interrupted"),
|
|
376
|
+
ToolExecutionError: (e) => showBanner(`Tool ${e.name} failed`),
|
|
377
|
+
}),
|
|
378
|
+
)
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## Testing
|
|
382
|
+
|
|
383
|
+
The bundled `test/helpers.ts` exposes a scriptable stub provider:
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
import { Effect, Layer, Stream } from "effect"
|
|
387
|
+
import { AIProvider, type AIProviderImpl } from "@hypen-space/gloop-effect"
|
|
388
|
+
|
|
389
|
+
const stub: AIProviderImpl = {
|
|
390
|
+
name: "stub",
|
|
391
|
+
complete: () => Effect.succeed({ id: "x", model: "stub", content: "ok", finishReason: "stop" }),
|
|
392
|
+
stream: () => ({
|
|
393
|
+
chunks: Stream.fromIterable(["o", "k"]),
|
|
394
|
+
result: Effect.succeed({ id: "x", model: "stub", content: "ok", finishReason: "stop" }),
|
|
395
|
+
cancel: Effect.void,
|
|
396
|
+
}),
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
Effect.runPromise(
|
|
400
|
+
Effect.scoped(program).pipe(
|
|
401
|
+
Effect.provide(Layer.succeed(AIProvider, stub)),
|
|
402
|
+
),
|
|
403
|
+
)
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
See `test/` for 21 tests covering: actor lifecycle (`interrupt`, `stop`, `awaitIdle`), error escalation (`Error` vs `Fatal`), tool execution + `ToolStart`/`ToolDone` pairing, confirm approve / deny, skill auto-registration, and conversation history.
|
|
407
|
+
|
|
408
|
+
## What's shared with `gloop-loop`
|
|
409
|
+
|
|
410
|
+
Pure / data / helpers are imported directly — no duplicate source of truth:
|
|
411
|
+
|
|
412
|
+
- `Form` ADT (`Think`, `Invoke`, `Confirm`, `Ask`, `Remember`, `Forget`, `Emit`, `Refresh`, `Done`, `Seq`, `Nil`, `Install`, `ListTools`, `Spawn`) and the interpreter dispatch (`toolCallsToForm`, `formatResults`, `parseInput`)
|
|
413
|
+
- Skill parsing/formatting (`parseSkillMarkdown`, `findSkill`, `mergeSkillsIntoSystem`, `formatSkillsListing`, `applySkillSubstitutions`, `splitSkillArguments`, `matchSkillSlash`, `skillInvocationToThinkInput`, `thinkInputFromSkillSubcommand`, `createInvokeSkillTool`)
|
|
414
|
+
- Builtin tool bodies (wrapped by `toEffectTool` into Effect tools)
|
|
415
|
+
- `createNodeIO`, `createFileMemory` (wrapped with Effect adapters)
|
|
416
|
+
- OpenRouter HTTP logic (wrapped into the `AIProvider` layer)
|
|
417
|
+
|
|
418
|
+
Rebuilt on Effect primitives: the actor shell, provider interface, conversation state, tool registry, error types, and every public method signature.
|
|
419
|
+
|
|
420
|
+
## License
|
|
421
|
+
|
|
422
|
+
MIT
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gloop-effect/AIProvider — Effect-native provider interface.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the callable surface of an LLM provider as an Effect service.
|
|
5
|
+
* `complete` returns a plain Effect; `stream` returns a Stream of chunks
|
|
6
|
+
* plus an Effect for the finalized response (including tool calls).
|
|
7
|
+
*
|
|
8
|
+
* Concrete providers (OpenRouter, Anthropic, local) are registered by
|
|
9
|
+
* providing a layer at the application root.
|
|
10
|
+
*/
|
|
11
|
+
import { Context, Effect, Stream } from "effect";
|
|
12
|
+
import type { AIRequestConfig, AIResponse, JsonToolCall } from "@hypen-space/gloop-loop";
|
|
13
|
+
import type { AIProviderError } from "./Errors.js";
|
|
14
|
+
/**
|
|
15
|
+
* A streaming response. `chunks` yields text deltas as they arrive; `result`
|
|
16
|
+
* resolves once the stream is complete with tool calls and finish reason.
|
|
17
|
+
*
|
|
18
|
+
* Consumers typically drain `chunks` (to emit `stream_chunk` events) and
|
|
19
|
+
* then await `result` to pick up any tool calls.
|
|
20
|
+
*/
|
|
21
|
+
export interface StreamResponse {
|
|
22
|
+
readonly chunks: Stream.Stream<string, AIProviderError>;
|
|
23
|
+
readonly result: Effect.Effect<AIResponse, AIProviderError>;
|
|
24
|
+
/** Cancel an in-flight stream. Safe to call after the stream completes. */
|
|
25
|
+
readonly cancel: Effect.Effect<void>;
|
|
26
|
+
}
|
|
27
|
+
export interface AIProviderImpl {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly complete: (config: AIRequestConfig) => Effect.Effect<AIResponse, AIProviderError>;
|
|
30
|
+
readonly stream: (config: AIRequestConfig) => StreamResponse;
|
|
31
|
+
}
|
|
32
|
+
declare const AIProvider_base: Context.TagClass<AIProvider, "gloop/AIProvider", AIProviderImpl>;
|
|
33
|
+
export declare class AIProvider extends AIProvider_base {
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Drain a `StreamResponse`: emit each chunk through `onChunk`, then resolve
|
|
37
|
+
* with the final `AIResponse`. Interruption cancels the underlying stream.
|
|
38
|
+
*/
|
|
39
|
+
export declare const consumeStream: (response: StreamResponse, onChunk: (text: string) => Effect.Effect<void>) => Effect.Effect<AIResponse, AIProviderError>;
|
|
40
|
+
/** Extract just the tool calls from a completed AIResponse. */
|
|
41
|
+
export declare const toolCallsOf: (response: AIResponse) => ReadonlyArray<JsonToolCall>;
|
|
42
|
+
export {};
|
|
43
|
+
//# sourceMappingURL=AIProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AIProvider.d.ts","sourceRoot":"","sources":["../src/AIProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAChD,OAAO,KAAK,EACV,eAAe,EACf,UAAU,EACV,YAAY,EACb,MAAM,yBAAyB,CAAA;AAChC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAMlD;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;IACvD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,eAAe,CAAC,CAAA;IAC3D,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;CACrC;AAMD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,CACjB,MAAM,EAAE,eAAe,KACpB,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,eAAe,CAAC,CAAA;IAC/C,QAAQ,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,cAAc,CAAA;CAC7D;;AAED,qBAAa,UAAW,SAAQ,eAG7B;CAAG;AAMN;;;GAGG;AACH,eAAO,MAAM,aAAa,GACxB,UAAU,cAAc,EACxB,SAAS,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,KAC7C,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,eAAe,CAMzC,CAAA;AAEH,+DAA+D;AAC/D,eAAO,MAAM,WAAW,GAAI,UAAU,UAAU,KAAG,aAAa,CAAC,YAAY,CACnD,CAAA"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gloop-effect/AIProvider — Effect-native provider interface.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the callable surface of an LLM provider as an Effect service.
|
|
5
|
+
* `complete` returns a plain Effect; `stream` returns a Stream of chunks
|
|
6
|
+
* plus an Effect for the finalized response (including tool calls).
|
|
7
|
+
*
|
|
8
|
+
* Concrete providers (OpenRouter, Anthropic, local) are registered by
|
|
9
|
+
* providing a layer at the application root.
|
|
10
|
+
*/
|
|
11
|
+
import { Context, Effect, Stream } from "effect";
|
|
12
|
+
export class AIProvider extends Context.Tag("gloop/AIProvider")() {
|
|
13
|
+
}
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Helpers for consumers
|
|
16
|
+
// ============================================================================
|
|
17
|
+
/**
|
|
18
|
+
* Drain a `StreamResponse`: emit each chunk through `onChunk`, then resolve
|
|
19
|
+
* with the final `AIResponse`. Interruption cancels the underlying stream.
|
|
20
|
+
*/
|
|
21
|
+
export const consumeStream = (response, onChunk) => Effect.gen(function* () {
|
|
22
|
+
yield* response.chunks.pipe(Stream.runForEach(onChunk));
|
|
23
|
+
return yield* response.result;
|
|
24
|
+
}).pipe(Effect.onInterrupt(() => response.cancel));
|
|
25
|
+
/** Extract just the tool calls from a completed AIResponse. */
|
|
26
|
+
export const toolCallsOf = (response) => response.toolCalls ?? [];
|
|
27
|
+
//# sourceMappingURL=AIProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AIProvider.js","sourceRoot":"","sources":["../src/AIProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAsChD,MAAM,OAAO,UAAW,SAAQ,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,EAG5D;CAAG;AAEN,+EAA+E;AAC/E,wBAAwB;AACxB,+EAA+E;AAE/E;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAC3B,QAAwB,EACxB,OAA8C,EACF,EAAE,CAC9C,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,KAAK,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAA;IACvD,OAAO,KAAK,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAA;AAC/B,CAAC,CAAC,CAAC,IAAI,CACL,MAAM,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAC1C,CAAA;AAEH,+DAA+D;AAC/D,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,QAAoB,EAA+B,EAAE,CAC/E,QAAQ,CAAC,SAAS,IAAI,EAAE,CAAA"}
|
package/dist/Agent.d.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gloop-effect/Agent — Effect-native replacement for `AgentLoop`.
|
|
3
|
+
*
|
|
4
|
+
* Actor model:
|
|
5
|
+
* - `Queue<AgentMessage>` is the inbox.
|
|
6
|
+
* - `PubSub<AgentEvent>` is the event bus; `events` is a Stream over it.
|
|
7
|
+
* - A single loop fiber drains the inbox, one turn at a time.
|
|
8
|
+
* - Each turn forks a child fiber so `interrupt` can target just that turn.
|
|
9
|
+
*
|
|
10
|
+
* Lifecycle:
|
|
11
|
+
* - `Agent.make(opts)` is scoped — call it inside an `Effect.scoped` or
|
|
12
|
+
* `Layer.scoped` so the loop fiber is cleaned up when the scope closes.
|
|
13
|
+
* - `interrupt` fires the current turn's fiber; the loop keeps running.
|
|
14
|
+
* - `stop` interrupts the loop and drains the inbox.
|
|
15
|
+
*/
|
|
16
|
+
import { Effect, Scope, Stream } from "effect";
|
|
17
|
+
import type { Skill, SpawnResult } from "@hypen-space/gloop-loop";
|
|
18
|
+
import { AIProvider } from "./AIProvider.js";
|
|
19
|
+
import { type ConversationHandle } from "./Conversation.js";
|
|
20
|
+
import { type AnyTool, type Tool, type ToolRegistry } from "./Tool.js";
|
|
21
|
+
import { type LoopConfig } from "./Interpreter.js";
|
|
22
|
+
import { type AgentError } from "./Errors.js";
|
|
23
|
+
import type { MessageId, ModelId, RequestId } from "./Schema.js";
|
|
24
|
+
import type { AgentEvent, AgentEventOf, AgentMessage, AgentMessageRole, EnqueuedAgentMessage } from "./Schema.js";
|
|
25
|
+
export type { AgentEvent, AgentEventOf, AgentMessage, AgentMessageRole, EnqueuedAgentMessage, };
|
|
26
|
+
export interface AgentMakeOptions {
|
|
27
|
+
readonly model: ModelId | string;
|
|
28
|
+
readonly system?: string;
|
|
29
|
+
readonly skills?: ReadonlyArray<Skill>;
|
|
30
|
+
readonly tools?: ReadonlyArray<AnyTool>;
|
|
31
|
+
readonly maxTokens?: number;
|
|
32
|
+
readonly contextPruneInterval?: number;
|
|
33
|
+
readonly classifySpawn?: LoopConfig["classifySpawn"];
|
|
34
|
+
/** Override the default `confirm_request` + `respondToConfirm` handshake. */
|
|
35
|
+
readonly confirm?: (command: string) => Effect.Effect<boolean>;
|
|
36
|
+
/** Override the default `ask_request` + `respondToAsk` handshake. */
|
|
37
|
+
readonly ask?: (question: string) => Effect.Effect<string>;
|
|
38
|
+
readonly remember?: (content: string) => Effect.Effect<void>;
|
|
39
|
+
readonly forget?: (content: string) => Effect.Effect<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Rebuild the base system prompt. The returned string is fed back through
|
|
42
|
+
* `mergeSkillsIntoSystem` before being applied. Return `void` to keep the
|
|
43
|
+
* existing prompt and just re-emit `system_refreshed`.
|
|
44
|
+
*/
|
|
45
|
+
readonly refreshSystem?: () => Effect.Effect<string | void>;
|
|
46
|
+
readonly manageContext?: (instructions: string) => Effect.Effect<string>;
|
|
47
|
+
readonly installTool?: (source: string) => Effect.Effect<string>;
|
|
48
|
+
readonly listTools?: () => Effect.Effect<string>;
|
|
49
|
+
readonly spawn?: (task: string) => Effect.Effect<SpawnResult>;
|
|
50
|
+
/** Classify an error as fatal. Fatal errors stop the loop. */
|
|
51
|
+
readonly isFatal?: (error: AgentError) => boolean;
|
|
52
|
+
readonly log?: (label: string, content: string) => Effect.Effect<void>;
|
|
53
|
+
}
|
|
54
|
+
export interface Agent {
|
|
55
|
+
/** Enqueue a message. Returns its `MessageId` for correlation. */
|
|
56
|
+
readonly send: (msg: AgentMessage | string) => Effect.Effect<MessageId>;
|
|
57
|
+
/**
|
|
58
|
+
* Enqueue and await the turn's completion. Fails with the turn's
|
|
59
|
+
* `AgentError`, or `AgentInterruptedError` if the turn was interrupted.
|
|
60
|
+
*/
|
|
61
|
+
readonly sendSync: (msg: AgentMessage | string) => Effect.Effect<void, AgentError>;
|
|
62
|
+
/** Full event firehose. Each call creates a fresh subscription. */
|
|
63
|
+
readonly events: Stream.Stream<AgentEvent>;
|
|
64
|
+
/** Filtered stream of a single event tag. */
|
|
65
|
+
readonly eventsOf: <T extends AgentEvent["_tag"]>(tag: T) => Stream.Stream<AgentEventOf<T>>;
|
|
66
|
+
/** Interrupt the current turn. The loop keeps running. */
|
|
67
|
+
readonly interrupt: Effect.Effect<void>;
|
|
68
|
+
/** Stop the loop, drain the inbox. */
|
|
69
|
+
readonly stop: Effect.Effect<void>;
|
|
70
|
+
/** Resolves when the inbox is empty *and* no turn is running. */
|
|
71
|
+
readonly awaitIdle: Effect.Effect<void>;
|
|
72
|
+
readonly pending: Effect.Effect<number>;
|
|
73
|
+
/** Register / update a tool; takes effect next turn. */
|
|
74
|
+
readonly addTool: <E extends import("./Errors.js").AgentError>(tool: Tool<E>) => Effect.Effect<void>;
|
|
75
|
+
readonly removeTool: (name: string) => Effect.Effect<void>;
|
|
76
|
+
readonly setTools: (tools: ReadonlyArray<AnyTool>) => Effect.Effect<void>;
|
|
77
|
+
readonly setSystem: (prompt: string) => Effect.Effect<void>;
|
|
78
|
+
readonly clear: Effect.Effect<void>;
|
|
79
|
+
/** Resolve a pending `ConfirmRequest`. */
|
|
80
|
+
readonly respondToConfirm: (id: RequestId, ok: boolean) => Effect.Effect<void>;
|
|
81
|
+
/** Resolve a pending `AskRequest`. */
|
|
82
|
+
readonly respondToAsk: (id: RequestId, answer: string) => Effect.Effect<void>;
|
|
83
|
+
/** Snapshot access for advanced callers. */
|
|
84
|
+
readonly registry: ToolRegistry;
|
|
85
|
+
readonly conversation: ConversationHandle;
|
|
86
|
+
}
|
|
87
|
+
export declare const make: (options: AgentMakeOptions) => Effect.Effect<Agent, never, AIProvider | Scope.Scope>;
|
|
88
|
+
export declare const Agent: {
|
|
89
|
+
make: (options: AgentMakeOptions) => Effect.Effect<Agent, never, AIProvider | Scope.Scope>;
|
|
90
|
+
};
|
|
91
|
+
//# sourceMappingURL=Agent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Agent.d.ts","sourceRoot":"","sources":["../src/Agent.ts"],"names":[],"mappings":"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"}
|