@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.
Files changed (46) hide show
  1. package/README.md +422 -0
  2. package/dist/AIProvider.d.ts +43 -0
  3. package/dist/AIProvider.d.ts.map +1 -0
  4. package/dist/AIProvider.js +27 -0
  5. package/dist/AIProvider.js.map +1 -0
  6. package/dist/Agent.d.ts +91 -0
  7. package/dist/Agent.d.ts.map +1 -0
  8. package/dist/Agent.js +378 -0
  9. package/dist/Agent.js.map +1 -0
  10. package/dist/Conversation.d.ts +44 -0
  11. package/dist/Conversation.d.ts.map +1 -0
  12. package/dist/Conversation.js +124 -0
  13. package/dist/Conversation.js.map +1 -0
  14. package/dist/Errors.d.ts +102 -0
  15. package/dist/Errors.d.ts.map +1 -0
  16. package/dist/Errors.js +80 -0
  17. package/dist/Errors.js.map +1 -0
  18. package/dist/Interpreter.d.ts +62 -0
  19. package/dist/Interpreter.d.ts.map +1 -0
  20. package/dist/Interpreter.js +217 -0
  21. package/dist/Interpreter.js.map +1 -0
  22. package/dist/Schema.d.ts +188 -0
  23. package/dist/Schema.d.ts.map +1 -0
  24. package/dist/Schema.js +135 -0
  25. package/dist/Schema.js.map +1 -0
  26. package/dist/Tool.d.ts +70 -0
  27. package/dist/Tool.d.ts.map +1 -0
  28. package/dist/Tool.js +138 -0
  29. package/dist/Tool.js.map +1 -0
  30. package/dist/defaults/Builtins.d.ts +23 -0
  31. package/dist/defaults/Builtins.d.ts.map +1 -0
  32. package/dist/defaults/Builtins.js +38 -0
  33. package/dist/defaults/Builtins.js.map +1 -0
  34. package/dist/defaults/FileMemory.d.ts +16 -0
  35. package/dist/defaults/FileMemory.d.ts.map +1 -0
  36. package/dist/defaults/FileMemory.js +32 -0
  37. package/dist/defaults/FileMemory.js.map +1 -0
  38. package/dist/defaults/OpenRouter.d.ts +20 -0
  39. package/dist/defaults/OpenRouter.d.ts.map +1 -0
  40. package/dist/defaults/OpenRouter.js +68 -0
  41. package/dist/defaults/OpenRouter.js.map +1 -0
  42. package/dist/index.d.ts +21 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +42 -0
  45. package/dist/index.js.map +1 -0
  46. 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"}
@@ -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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"}