@fungi.computer/mule 0.0.0-stage → 0.0.1

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.
@@ -0,0 +1,45 @@
1
+ import type { Message, Tool, ToolCall } from "@earendil-works/pi-ai";
2
+ import type { MuleCompletedTurn, MuleEvent, MuleRunOutcome, MuleRunSettings, MuleToolExecutionPart, MuleToolRejection } from "./contract.js";
3
+ export type { MuleCompletedTurn, MuleResume, MuleToolCheckpoint, MuleError, MuleEvent, MuleIdentity, MuleIdentityKind, MuleRunOutcome, MuleRunSettings, MuleStreamFunction, MuleToolExecutionPart, MuleToolRejection, MuleToolResult, } from "./contract.js";
4
+ /** Promise listener adapted once into Mule's canonical Effect program. */
5
+ export type MulePromiseListener = (event: MuleEvent, signal: AbortSignal) => Promise<void> | void;
6
+ /** Outcome of preparing one exact Pi tool call. */
7
+ export type MuleToolPreparation = {
8
+ readonly status: "prepared";
9
+ /** Run the already-decoded and authorized call; abort ends it early. */
10
+ readonly execute: (signal: AbortSignal) => AsyncIterable<MuleToolExecutionPart>;
11
+ } | ({
12
+ readonly status: "rejected";
13
+ } & MuleToolRejection);
14
+ /** Pi tool declaration paired with its Promise preparation door. */
15
+ export type MuleTool = {
16
+ readonly descriptor: Tool;
17
+ /** Only read-only or explicitly idempotent tools may opt into crash replay. */
18
+ readonly replay?: "safe" | "unsafe" | undefined;
19
+ readonly executionMode?: "parallel" | "sequential" | undefined;
20
+ /** Decode and authorize one exact Pi tool call before it can execute. */
21
+ readonly prepare: (call: ToolCall, signal: AbortSignal) => Promise<MuleToolPreparation>;
22
+ };
23
+ /** Inputs for one complete Pi model/tool loop through the Promise facade. */
24
+ export type MuleRunInput = MuleRunSettings & {
25
+ readonly tools?: readonly MuleTool[] | undefined;
26
+ /** Resolve a fresh provider credential immediately before each Pi call. */
27
+ readonly resolveApiKey?: ((provider: string, signal: AbortSignal) => Promise<string | undefined>) | undefined;
28
+ /** Pull messages that should steer the active run at Pi's safe point. */
29
+ readonly getSteeringMessages?: ((signal: AbortSignal) => Promise<readonly Message[]>) | undefined;
30
+ /** Pull messages queued until the run would otherwise finish. */
31
+ readonly getFollowUpMessages?: ((signal: AbortSignal) => Promise<readonly Message[]>) | undefined;
32
+ /** Request a graceful stop after the current Pi turn has fully settled. */
33
+ readonly shouldStopAfterTurn?: ((turn: MuleCompletedTurn, signal: AbortSignal) => Promise<boolean>) | undefined;
34
+ };
35
+ /** Promise-facing Mule interface. */
36
+ export type Mule = {
37
+ /**
38
+ * Run one complete Pi model/tool loop. Rejects with the `MuleError` that
39
+ * failed the run, or with the abort reason (an `AbortError` when the reason
40
+ * is not an `Error`) after the caller aborts `signal`.
41
+ */
42
+ readonly run: (input: MuleRunInput, listener?: MulePromiseListener, signal?: AbortSignal) => Promise<MuleRunOutcome>;
43
+ };
44
+ /** Default Promise-facing Mule interface. */
45
+ export declare const mule: Mule;
package/dist/index.js ADDED
@@ -0,0 +1,97 @@
1
+ import * as Cause from "effect/Cause";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Exit from "effect/Exit";
4
+ import * as Option from "effect/Option";
5
+ import * as Stream from "effect/Stream";
6
+ import { run as runEffect, } from "./effect.js";
7
+ function fromPromise(phase, message, evaluate) {
8
+ return Effect.tryPromise({
9
+ try: (signal) => Promise.resolve(evaluate(signal)),
10
+ catch: (cause) => ({ _tag: "MuleError", phase, message, cause }),
11
+ });
12
+ }
13
+ function executionStream(call, execute) {
14
+ // Effect closes the iterator with `return()`, which an async generator only
15
+ // honours at its next yield. Abort first so a tool awaiting its signal can
16
+ // reach that point instead of holding interruption open.
17
+ const execution = {
18
+ [Symbol.asyncIterator]() {
19
+ const controller = new AbortController();
20
+ const iterator = execute(controller.signal)[Symbol.asyncIterator]();
21
+ return {
22
+ next: () => iterator.next(),
23
+ return: async () => {
24
+ controller.abort();
25
+ return ((await iterator.return?.()) ?? { done: true, value: undefined });
26
+ },
27
+ };
28
+ },
29
+ };
30
+ return Stream.fromAsyncIterable(execution, (cause) => ({
31
+ _tag: "MuleError",
32
+ phase: "tool",
33
+ message: `Tool "${call.name}" failed`,
34
+ cause,
35
+ }));
36
+ }
37
+ function liftTool(tool) {
38
+ return {
39
+ descriptor: tool.descriptor,
40
+ executionMode: tool.executionMode,
41
+ replay: tool.replay,
42
+ prepare: (call) => Effect.tryPromise({
43
+ try: (signal) => tool.prepare(call, signal),
44
+ catch: () => ({
45
+ message: `Tool "${call.name}" could not be prepared`,
46
+ }),
47
+ }).pipe(Effect.flatMap((preparation) => preparation.status === "rejected"
48
+ ? Effect.fail({
49
+ message: preparation.message,
50
+ terminate: preparation.terminate,
51
+ })
52
+ : Effect.succeed({
53
+ execute: () => executionStream(call, preparation.execute),
54
+ }))),
55
+ };
56
+ }
57
+ /** Adapt the complete Promise port set once into the canonical Effect ports. */
58
+ function liftInput(input) {
59
+ const { tools, resolveApiKey, getSteeringMessages, getFollowUpMessages, shouldStopAfterTurn, ...settings } = input;
60
+ return {
61
+ ...settings,
62
+ tools: tools?.map(liftTool),
63
+ resolveApiKey: resolveApiKey &&
64
+ ((provider) => fromPromise("model", "Mule could not resolve the API key", (signal) => resolveApiKey(provider, signal))),
65
+ getSteeringMessages: getSteeringMessages &&
66
+ (() => fromPromise("control", "Mule could not read steering messages", getSteeringMessages)),
67
+ getFollowUpMessages: getFollowUpMessages &&
68
+ (() => fromPromise("control", "Mule could not read follow-up messages", getFollowUpMessages)),
69
+ shouldStopAfterTurn: shouldStopAfterTurn &&
70
+ ((turn) => fromPromise("control", "Mule stop hook failed", (signal) => shouldStopAfterTurn(turn, signal))),
71
+ };
72
+ }
73
+ const noListener = () => undefined;
74
+ async function run(input, listener = noListener, signal) {
75
+ if (signal?.aborted === true)
76
+ throw abortReason(signal);
77
+ const program = runEffect(liftInput(input), (event) => fromPromise("listener", "Mule listener failed", (listenerSignal) => listener(event, listenerSignal)));
78
+ const exit = await Effect.runPromiseExit(program, signal === undefined ? undefined : { signal });
79
+ if (Exit.isSuccess(exit))
80
+ return exit.value;
81
+ const error = Cause.findErrorOption(exit.cause);
82
+ // Preserve the facade's documented typed MuleError rejection.
83
+ if (Option.isSome(error))
84
+ return Effect.runPromise(Effect.fail(error.value));
85
+ if (signal !== undefined && Cause.hasInterruptsOnly(exit.cause))
86
+ throw abortReason(signal);
87
+ throw Cause.squash(exit.cause);
88
+ }
89
+ /** The caller's abort reason, as `fetch` rejects; never a non-Error value. */
90
+ function abortReason(signal) {
91
+ const reason = signal.reason;
92
+ return reason instanceof Error
93
+ ? reason
94
+ : new DOMException("The operation was aborted", "AbortError");
95
+ }
96
+ /** Default Promise-facing Mule interface. */
97
+ export const mule = { run };
@@ -0,0 +1,15 @@
1
+ import { type FauxProviderHandle, type FauxResponseStep, type RegisterFauxProviderOptions } from "@earendil-works/pi-ai/providers/faux";
2
+ import type { Api, Model } from "@earendil-works/pi-ai";
3
+ /** Deterministic model and stream with Pi's observable response state. */
4
+ export type FauxModel = Readonly<{
5
+ /** Model supplied to Mule's run input. */
6
+ model: Model<Api>;
7
+ /** Bound Pi faux stream; never calls a remote provider. */
8
+ stream: FauxProviderHandle["provider"]["streamSimple"];
9
+ /** Replace the responses consumed by subsequent assistant calls. */
10
+ setResponses: (replies: readonly FauxResponseStep[]) => void;
11
+ /** Pi's call history and counters for deterministic assertions. */
12
+ state: FauxProviderHandle["state"];
13
+ }>;
14
+ /** Create a credential-free model for tests and runnable examples. */
15
+ export declare function fauxModel(replies: readonly FauxResponseStep[], options?: RegisterFauxProviderOptions): FauxModel;
@@ -0,0 +1,14 @@
1
+ import { fauxProvider, } from "@earendil-works/pi-ai/providers/faux";
2
+ /** Create a credential-free model for tests and runnable examples. */
3
+ export function fauxModel(replies, options = {}) {
4
+ const faux = fauxProvider(options);
5
+ faux.setResponses([...replies]);
6
+ return {
7
+ model: faux.getModel(),
8
+ stream: faux.provider.streamSimple.bind(faux.provider),
9
+ setResponses: (responses) => {
10
+ faux.setResponses([...responses]);
11
+ },
12
+ state: faux.state,
13
+ };
14
+ }
@@ -0,0 +1,15 @@
1
+ <!-- Generated by Botanical API docs (TypeDoc + typedoc-plugin-markdown). Do not edit. -->
2
+
3
+ **@fungi.computer/mule**
4
+
5
+ ---
6
+
7
+ # @fungi.computer/mule
8
+
9
+ ## Modules
10
+
11
+ | Module | Description |
12
+ | --------------------- | ----------- |
13
+ | [effect](effect.md) | - |
14
+ | [index](index.md) | - |
15
+ | [testing](testing.md) | - |
@@ -0,0 +1,350 @@
1
+ <!-- Generated by Botanical API docs (TypeDoc + typedoc-plugin-markdown). Do not edit. -->
2
+
3
+ [**@fungi.computer/mule**](README.md)
4
+
5
+ ---
6
+
7
+ [@fungi.computer/mule](README.md) / effect
8
+
9
+ # effect
10
+
11
+ ## Type Aliases
12
+
13
+ ### MuleRetryObservation
14
+
15
+ ```ts
16
+ type MuleRetryObservation =
17
+ | {
18
+ type: "retry_scheduled";
19
+ run: MuleIdentity;
20
+ turn: MuleIdentity;
21
+ attempt: number;
22
+ maxAttempts: number;
23
+ delayMs: number;
24
+ errorMessage: string;
25
+ }
26
+ | {
27
+ type: "retry_attempt_started";
28
+ run: MuleIdentity;
29
+ turn: MuleIdentity;
30
+ attempt: number;
31
+ }
32
+ | {
33
+ type: "retry_finished";
34
+ run: MuleIdentity;
35
+ turn: MuleIdentity;
36
+ attempt: number;
37
+ success: boolean;
38
+ finalError?: string;
39
+ };
40
+ ```
41
+
42
+ One observed Pi-retry event around a single assistant call.
43
+
44
+ ---
45
+
46
+ ### MulePreparedTool
47
+
48
+ ```ts
49
+ type MulePreparedTool = object;
50
+ ```
51
+
52
+ A decoded, authorized invocation ready to execute.
53
+
54
+ #### Properties
55
+
56
+ ##### execute
57
+
58
+ ```ts
59
+ readonly execute: () => Stream.Stream<MuleToolExecutionPart, MuleError>;
60
+ ```
61
+
62
+ Run the already-decoded and authorized tool call.
63
+
64
+ ###### Returns
65
+
66
+ `Stream.Stream`\<[`MuleToolExecutionPart`](index.md#muletoolexecutionpart),
67
+ [`MuleError`](index.md#muleerror)\>
68
+
69
+ ---
70
+
71
+ ### MuleTool
72
+
73
+ ```ts
74
+ type MuleTool = object;
75
+ ```
76
+
77
+ Pi tool declaration paired with its Effect-native preparation door.
78
+
79
+ #### Properties
80
+
81
+ ##### descriptor
82
+
83
+ ```ts
84
+ readonly descriptor: Tool;
85
+ ```
86
+
87
+ ##### replay?
88
+
89
+ ```ts
90
+ readonly optional replay?: "safe" | "unsafe";
91
+ ```
92
+
93
+ Only read-only or explicitly idempotent tools may opt into crash replay.
94
+
95
+ ##### executionMode?
96
+
97
+ ```ts
98
+ readonly optional executionMode?: "parallel" | "sequential";
99
+ ```
100
+
101
+ ##### prepare
102
+
103
+ ```ts
104
+ readonly prepare: (call) => Effect.Effect<MulePreparedTool, MuleToolRejection>;
105
+ ```
106
+
107
+ Decode and authorize one exact Pi tool call before it can execute.
108
+
109
+ ###### Parameters
110
+
111
+ | Parameter | Type |
112
+ | --------- | ---------- |
113
+ | `call` | `ToolCall` |
114
+
115
+ ###### Returns
116
+
117
+ `Effect.Effect`\<[`MulePreparedTool`](#mulepreparedtool),
118
+ [`MuleToolRejection`](index.md#muletoolrejection)\>
119
+
120
+ ---
121
+
122
+ ### MuleListener
123
+
124
+ ```ts
125
+ type MuleListener = (event) => Effect.Effect<void, MuleError>;
126
+ ```
127
+
128
+ Effect-native listener. Completion is part of run settlement.
129
+
130
+ #### Parameters
131
+
132
+ | Parameter | Type |
133
+ | --------- | --------------------------------- |
134
+ | `event` | [`MuleEvent`](index.md#muleevent) |
135
+
136
+ #### Returns
137
+
138
+ `Effect.Effect`\<`void`, [`MuleError`](index.md#muleerror)\>
139
+
140
+ ---
141
+
142
+ ### MuleRunInput
143
+
144
+ ```ts
145
+ type MuleRunInput = MuleRunSettings & object;
146
+ ```
147
+
148
+ Inputs for one complete Pi model/tool loop. Cancel a run by interrupting its
149
+ fiber; the run then ends with a `cancelled` `run_end`.
150
+
151
+ #### Type Declaration
152
+
153
+ | Name | Type | Description |
154
+ | ------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
155
+ | `prepareSystemPrompt()?` | () => `Effect.Effect`\<`string`, [`MuleError`](index.md#muleerror)\> | Rebuild the model system prompt immediately before each provider request. |
156
+ | `tools?` | readonly [`MuleTool`](#muletool)[] | - |
157
+ | `resolveApiKey()?` | (`provider`) => `Effect.Effect`\<`string` \| `undefined`, [`MuleError`](index.md#muleerror)\> | Resolve a fresh provider credential immediately before each Pi call. |
158
+ | `getSteeringMessages()?` | () => `Effect.Effect`\<readonly `Message`[], [`MuleError`](index.md#muleerror)\> | Pull messages that should steer the active run at Pi's safe point. |
159
+ | `getFollowUpMessages()?` | () => `Effect.Effect`\<readonly `Message`[], [`MuleError`](index.md#muleerror)\> | Pull messages queued until the run would otherwise finish. |
160
+ | `prepareNextTurn()?` | (`turn`) => `Effect.Effect`\<readonly `Message`[] \| `undefined`, [`MuleError`](index.md#muleerror)\> | Pi's between-turn point: after a turn's tool results and before the next assistant request of the same run. Return a replacement context, such as one rebuilt after compaction, or `undefined` to keep the current one. |
161
+ | `shouldStopAfterTurn()?` | (`turn`) => `Effect.Effect`\<`boolean`, [`MuleError`](index.md#muleerror)\> | Request a graceful stop after the current Pi turn has fully settled. |
162
+ | `onRetry()?` | (`event`) => `Effect.Effect`\<`void`, [`MuleError`](index.md#muleerror)\> | Effect-native observation sink for Pi's retry policy (`retry`), one call per scheduled or finished retry. |
163
+
164
+ ---
165
+
166
+ ### MuleEffect
167
+
168
+ ```ts
169
+ type MuleEffect = object;
170
+ ```
171
+
172
+ Canonical Effect-native Mule interface.
173
+
174
+ #### Properties
175
+
176
+ ##### run
177
+
178
+ ```ts
179
+ readonly run: (input, listener?) => Effect.Effect<MuleRunOutcome, MuleError>;
180
+ ```
181
+
182
+ Run one complete Pi model/tool loop.
183
+
184
+ ###### Parameters
185
+
186
+ | Parameter | Type |
187
+ | ----------- | ------------------------------- |
188
+ | `input` | [`MuleRunInput`](#muleruninput) |
189
+ | `listener?` | [`MuleListener`](#mulelistener) |
190
+
191
+ ###### Returns
192
+
193
+ `Effect.Effect`\<[`MuleRunOutcome`](index.md#mulerunoutcome),
194
+ [`MuleError`](index.md#muleerror)\>
195
+
196
+ ## Variables
197
+
198
+ ### piCodingAgentRetryDefaults
199
+
200
+ ```ts
201
+ const piCodingAgentRetryDefaults: RetryPolicy;
202
+ ```
203
+
204
+ Pi coding-agent's retry defaults (`settings-manager.js` `getRetrySettings()`,
205
+ `@earendil-works/pi-ai` 0.99.0, the version pinned in the lockfile). Pi-ai's own
206
+ `RetryPolicy` has no default — an undefined policy means no retry — so a host
207
+ that wants Pi coding-agent's numbers passes this explicitly.
208
+
209
+ ---
210
+
211
+ ### piHttpIdleTimeoutMs
212
+
213
+ ```ts
214
+ const piHttpIdleTimeoutMs: 300000 = 300_000;
215
+ ```
216
+
217
+ Pi coding-agent's default stream idle timeout (`DEFAULT_HTTP_IDLE_TIMEOUT_MS`,
218
+ `settings-manager.js`). A host that wants Pi's number for
219
+ `modelStreamIdleTimeoutMs` passes this explicitly.
220
+
221
+ ---
222
+
223
+ ### muleEffect
224
+
225
+ ```ts
226
+ const muleEffect: MuleEffect;
227
+ ```
228
+
229
+ Canonical Effect-native Mule value.
230
+
231
+ ## Functions
232
+
233
+ ### isContextOverflow()
234
+
235
+ ```ts
236
+ function isContextOverflow(message, contextWindow?): boolean;
237
+ ```
238
+
239
+ Apply Pi's maintained context-overflow semantics to an exact Pi message.
240
+
241
+ #### Parameters
242
+
243
+ | Parameter | Type |
244
+ | ---------------- | ------------------ |
245
+ | `message` | `AssistantMessage` |
246
+ | `contextWindow?` | `number` |
247
+
248
+ #### Returns
249
+
250
+ `boolean`
251
+
252
+ ---
253
+
254
+ ### run()
255
+
256
+ ```ts
257
+ function run(input, listener?): Effect<MuleRunOutcome, MuleError>;
258
+ ```
259
+
260
+ Run Pi's model/tool loop inside Effect without changing Pi's values.
261
+
262
+ #### Parameters
263
+
264
+ | Parameter | Type | Default value |
265
+ | ---------- | ------------------------------- | ------------- |
266
+ | `input` | [`MuleRunInput`](#muleruninput) | `undefined` |
267
+ | `listener` | [`MuleListener`](#mulelistener) | `noListener` |
268
+
269
+ #### Returns
270
+
271
+ `Effect`\<[`MuleRunOutcome`](index.md#mulerunoutcome),
272
+ [`MuleError`](index.md#muleerror)\>
273
+
274
+ ## References
275
+
276
+ ### MuleCompletedTurn
277
+
278
+ Re-exports [MuleCompletedTurn](index.md#mulecompletedturn)
279
+
280
+ ---
281
+
282
+ ### MuleResume
283
+
284
+ Re-exports [MuleResume](index.md#muleresume)
285
+
286
+ ---
287
+
288
+ ### MuleToolCheckpoint
289
+
290
+ Re-exports [MuleToolCheckpoint](index.md#muletoolcheckpoint)
291
+
292
+ ---
293
+
294
+ ### MuleError
295
+
296
+ Re-exports [MuleError](index.md#muleerror)
297
+
298
+ ---
299
+
300
+ ### MuleEvent
301
+
302
+ Re-exports [MuleEvent](index.md#muleevent)
303
+
304
+ ---
305
+
306
+ ### MuleIdentity
307
+
308
+ Re-exports [MuleIdentity](index.md#muleidentity)
309
+
310
+ ---
311
+
312
+ ### MuleIdentityKind
313
+
314
+ Re-exports [MuleIdentityKind](index.md#muleidentitykind)
315
+
316
+ ---
317
+
318
+ ### MuleRunOutcome
319
+
320
+ Re-exports [MuleRunOutcome](index.md#mulerunoutcome)
321
+
322
+ ---
323
+
324
+ ### MuleRunSettings
325
+
326
+ Re-exports [MuleRunSettings](index.md#mulerunsettings)
327
+
328
+ ---
329
+
330
+ ### MuleStreamFunction
331
+
332
+ Re-exports [MuleStreamFunction](index.md#mulestreamfunction)
333
+
334
+ ---
335
+
336
+ ### MuleToolExecutionPart
337
+
338
+ Re-exports [MuleToolExecutionPart](index.md#muletoolexecutionpart)
339
+
340
+ ---
341
+
342
+ ### MuleToolRejection
343
+
344
+ Re-exports [MuleToolRejection](index.md#muletoolrejection)
345
+
346
+ ---
347
+
348
+ ### MuleToolResult
349
+
350
+ Re-exports [MuleToolResult](index.md#muletoolresult)