@fungi.computer/mule 0.0.0-stage → 0.0.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jamstack Consulting LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,121 @@
1
- # Temporary Holding Version
1
+ # Mule (`@fungi.computer/mule`)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Mule runs Pi's model and tool loop inside Effect. Use it when your host already
4
+ owns the model, messages and tools and needs an awaited, cancellable run. Pi's
5
+ models, messages, provider replay fields and stream events keep their original
6
+ types.
7
+
8
+ ## Set up
9
+
10
+ ```sh
11
+ npm install @fungi.computer/mule
12
+ ```
13
+
14
+ The package ships ESM only and depends on `effect` and Pi
15
+ (`@earendil-works/pi-ai`) at runtime, plus `@fungi.computer/cairn` for IDs. Mule
16
+ does not require or install the MCP SDK. Consumers that type-check dependency
17
+ declarations (`skipLibCheck: false`) must install Pi’s Google GenAI optional
18
+ declaration dependency themselves:
19
+
20
+ ```sh
21
+ npm install --save-dev @modelcontextprotocol/sdk
22
+ ```
23
+
24
+ The root entrypoint exports the Promise facade, `mule.run`.
25
+ `@fungi.computer/mule/effect` exports the canonical `muleEffect.run`. The
26
+ Promise facade adapts tools, hooks and listeners into that same Effect program,
27
+ so public Promise declarations do not require Effect types.
28
+
29
+ ## Quick start without a provider account
30
+
31
+ This deterministic example uses the public testing helper. It makes no network
32
+ request and needs no credential.
33
+
34
+ ```ts
35
+ import { fauxAssistantMessage } from "@earendil-works/pi-ai/providers/faux";
36
+ import { mule } from "@fungi.computer/mule";
37
+ import { fauxModel } from "@fungi.computer/mule/testing";
38
+
39
+ const faux = fauxModel([fauxAssistantMessage("pong")]);
40
+ const outcome = await mule.run(
41
+ {
42
+ model: faux.model,
43
+ stream: faux.stream,
44
+ systemPrompt: "Reply once.",
45
+ messages: [{ role: "user", content: "ping", timestamp: 1 }],
46
+ },
47
+ () => {},
48
+ );
49
+ console.log(outcome.status);
50
+ ```
51
+
52
+ The result is `completed`. No active stream remains when the awaited run
53
+ settles. The [test helper](test/support.ts) uses this provider and input shape,
54
+ and [facade tests](test/promise-facade.test.ts) compare Promise and Effect
55
+ lifecycles. This fake provider checks orchestration rather than a real supplier.
56
+
57
+ For a real model, supply Pi's `Model<Api>` and either its normal `streamSimple`
58
+ or an explicit provider stream. The host supplies credentials and billing. Await
59
+ the listener: its completion is part of the run's lifecycle.
60
+
61
+ ## Tools and control
62
+
63
+ A tool pairs Pi's descriptor with `prepare(call, signal)`. Preparation decodes
64
+ and authorizes the exact call. It returns either a typed rejection or a prepared
65
+ executor. The Promise executor yields parts through an `AsyncIterable`. The
66
+ Effect executor uses a Stream. Every executor must observe cancellation.
67
+
68
+ Run, turn, message and tool events carry causal identities. They let the host
69
+ record which execution produced an observation. They do not create durable
70
+ Session storage. Hosts may select parallel or sequential tool execution, steer
71
+ active work and queue follow-up input through the existing control interfaces.
72
+
73
+ A tool opts into safe crash replay only when it is read-only or explicitly
74
+ idempotent. The host persists the checkpoints needed to use `resume`. Declaring
75
+ `replay: "safe"` does not make an external write idempotent. Pi's provider
76
+ messages and replay fields pass through unchanged.
77
+
78
+ ## Cancellation, retries and errors
79
+
80
+ Abort the Promise facade's signal or interrupt the Effect fiber. Mule aborts the
81
+ Pi stream and running tools and records one cancelled `run_end`. The Promise
82
+ facade preserves an `Error` abort reason and rejects with it. A non-`Error`
83
+ reason becomes an `AbortError`. An operation that ignores abort still needs its
84
+ host to account for the backing work.
85
+
86
+ Tool preparation rejections and execution failures become model-visible error
87
+ results, so the run can continue. Fatal model or control failures produce a
88
+ failed `run_end`. A failed listener cannot be trusted to record that terminal
89
+ event. Preserve the rejection and its cause when reporting it.
90
+
91
+ `retry` selects Pi's bounded retry policy for each assistant call. Omitting it
92
+ means no retry. Provider Retry-After evidence and retry observations use the
93
+ existing run interfaces. Mule does not choose a provider policy for your host.
94
+ `modelStreamIdleTimeoutMs` bounds silence between provider events.
95
+
96
+ Mule owns no model catalog, credential store, durable Session database,
97
+ compaction service or process-restart scheduler. Compose those in the host.
98
+
99
+ ## API and examples
100
+
101
+ Use the [generated API reference](docs/api/README.md) and the exports in
102
+ [package.json](package.json).
103
+ [Cancellation parity](test/cancellation-parity.test.ts),
104
+ [retry policy](test/retry-policy.test.ts) and
105
+ [tool batch](test/tool-batch.test.ts) tests exercise the shared loop.
106
+
107
+ [The prompt example](examples/prompt.ts) is an optional live-provider caller. It
108
+ requires `CAMEL_API_KEY` supplied through your local environment. Its endpoint
109
+ and model overrides belong to that example, not to Mule's API. It is not needed
110
+ for the deterministic quick start. Do not commit credentials.
111
+
112
+ ## Contribute and license
113
+
114
+ Add an orchestration regression using the existing faux provider. From the root:
115
+
116
+ ```sh
117
+ pnpm --dir packages/mule run test
118
+ pnpm --dir packages/mule run docs
119
+ ```
120
+
121
+ MIT. See [LICENSE](LICENSE) and [THIRD_PARTY_NOTICES](THIRD_PARTY_NOTICES).
@@ -0,0 +1,15 @@
1
+ Mule's direct runtime dependencies are Effect 3.22.1, Cairn, and
2
+ @earendil-works/pi-ai 0.84.4. Consult each dependency's distribution for its
3
+ license terms.
4
+
5
+ The Pi dependency is pinned from commit
6
+ 853a80d26c90a14c1886f0ebb8ffaae133ca2185 and distributed under the MIT
7
+ License, Copyright (c) 2025 Mario Zechner. Its independently archived source
8
+ boundary has SHA-256
9
+ dfb11f4c1c00e47003db12765371198e944c8b640c995a1dc1cf9130bc9e49ee.
10
+
11
+ `src/effect.ts` is an Effect-language adaptation of the model/tool loop in
12
+ @earendil-works/pi-agent-core 0.84.4 at that commit. It also preserves reviewed
13
+ behavior from Botanical's retained @shiit/agent-core 0.66.2 at commit
14
+ b6eabe5dfd3cac9514025d26abbe9a92b083f10b, distributed under the MIT License,
15
+ Copyright (c) 2024 fungi.computer.
@@ -0,0 +1,192 @@
1
+ import type { Api, AssistantMessage, AssistantMessageEvent, AssistantMessageEventStream, TranscriptContext, ImageContent, JsonValue, Message, Model, RetryPolicy, SimpleStreamOptions, TextContent, ToolCall, ToolResultMessage, Usage } from "@earendil-works/pi-ai";
2
+ /** Pi's model stream function, supplied explicitly by the caller. */
3
+ export type MuleStreamFunction = (model: Model<Api>, context: TranscriptContext, options?: SimpleStreamOptions) => AssistantMessageEventStream | Promise<AssistantMessageEventStream>;
4
+ /** Causal roles added around Pi's model and tool loop. */
5
+ export type MuleIdentityKind = "run" | "turn" | "message" | "tool_call" | "tool_result" | "terminal";
6
+ /** One run-local causal identity. */
7
+ export type MuleIdentity = {
8
+ readonly kind: MuleIdentityKind;
9
+ readonly value: string;
10
+ readonly parent?: MuleIdentity | undefined;
11
+ };
12
+ /** Expected failure from Mule's model, tool, control, or listener work. */
13
+ export type MuleError = {
14
+ readonly _tag: "MuleError";
15
+ readonly phase: "model" | "tool" | "control" | "listener";
16
+ readonly message: string;
17
+ /** The underlying rejection or thrown value, when one exists. */
18
+ readonly cause?: unknown;
19
+ };
20
+ /** Result produced by a tool before Mule forms Pi's correlated result message. */
21
+ export type MuleToolResult = {
22
+ readonly content: (TextContent | ImageContent)[];
23
+ readonly details?: JsonValue | undefined;
24
+ readonly usage?: Usage | undefined;
25
+ readonly isError: boolean;
26
+ readonly terminate?: boolean | undefined;
27
+ };
28
+ /** Progress or terminal output from one tool execution. */
29
+ export type MuleToolExecutionPart = {
30
+ readonly type: "update";
31
+ readonly result: MuleToolResult;
32
+ } | {
33
+ readonly type: "result";
34
+ readonly result: MuleToolResult;
35
+ };
36
+ /** A real parser or authorization rejection produced before tool execution. */
37
+ export type MuleToolRejection = {
38
+ readonly message: string;
39
+ readonly terminate?: boolean | undefined;
40
+ };
41
+ /** Facts from a completed Pi turn supplied to the retained stop hook. */
42
+ export type MuleCompletedTurn = {
43
+ readonly message: AssistantMessage;
44
+ readonly toolResults: readonly ToolResultMessage[];
45
+ readonly context: readonly Message[];
46
+ readonly newMessages: readonly Message[];
47
+ };
48
+ /** Lifecycle emitted around Pi values. Pi messages and events are not rewritten. */
49
+ export type MuleEvent = {
50
+ readonly type: "run_start";
51
+ readonly run: MuleIdentity;
52
+ } | {
53
+ /** Exact prompt sent on this model attempt, including after a retry. */
54
+ readonly type: "model_prompt";
55
+ readonly run: MuleIdentity;
56
+ readonly turn: MuleIdentity;
57
+ readonly attempt: number;
58
+ readonly systemPrompt: string;
59
+ } | {
60
+ readonly type: "turn_start";
61
+ readonly run: MuleIdentity;
62
+ readonly turn: MuleIdentity;
63
+ } | {
64
+ readonly type: "message_start";
65
+ readonly run: MuleIdentity;
66
+ readonly turn: MuleIdentity;
67
+ readonly message: MuleIdentity;
68
+ readonly value: Message;
69
+ } | {
70
+ readonly type: "message_update";
71
+ readonly run: MuleIdentity;
72
+ readonly turn: MuleIdentity;
73
+ readonly message: MuleIdentity;
74
+ readonly value: AssistantMessage;
75
+ readonly event: AssistantMessageEvent;
76
+ } | {
77
+ readonly type: "message_end";
78
+ readonly run: MuleIdentity;
79
+ readonly turn: MuleIdentity;
80
+ readonly message: MuleIdentity;
81
+ readonly value: Message;
82
+ } | {
83
+ readonly type: "tool_execution_start";
84
+ readonly replay: "safe" | "unsafe";
85
+ readonly run: MuleIdentity;
86
+ readonly turn: MuleIdentity;
87
+ readonly toolCall: MuleIdentity;
88
+ readonly call: ToolCall;
89
+ } | {
90
+ readonly type: "tool_execution_update";
91
+ readonly run: MuleIdentity;
92
+ readonly turn: MuleIdentity;
93
+ readonly toolCall: MuleIdentity;
94
+ readonly call: ToolCall;
95
+ readonly result: MuleToolResult;
96
+ } | {
97
+ readonly type: "tool_execution_end";
98
+ readonly run: MuleIdentity;
99
+ readonly turn: MuleIdentity;
100
+ readonly toolCall: MuleIdentity;
101
+ readonly call: ToolCall;
102
+ readonly result: MuleToolResult;
103
+ } | {
104
+ readonly type: "turn_end";
105
+ readonly run: MuleIdentity;
106
+ readonly turn: MuleIdentity;
107
+ readonly message: AssistantMessage;
108
+ readonly toolResults: readonly ToolResultMessage[];
109
+ } | {
110
+ /**
111
+ * The run's one terminal. A run interrupted after `run_start` ends
112
+ * `cancelled`; a run that fails with a non-listener `MuleError` ends
113
+ * `failed`. A listener failure ends the run without this event.
114
+ */
115
+ readonly type: "run_end";
116
+ readonly run: MuleIdentity;
117
+ readonly terminal: MuleIdentity;
118
+ readonly outcome: "completed" | "cancelled" | "failed";
119
+ };
120
+ /** Durable evidence for a call in an interrupted assistant turn. */
121
+ export type MuleToolCheckpoint = {
122
+ readonly call: ToolCall;
123
+ readonly state: "pending";
124
+ } | {
125
+ readonly call: ToolCall;
126
+ readonly state: "started";
127
+ readonly replay: "safe" | "unsafe";
128
+ } | {
129
+ readonly call: ToolCall;
130
+ readonly state: "completed";
131
+ readonly result: MuleToolResult;
132
+ /** Present when the transcript message was committed too. */
133
+ readonly message?: ToolResultMessage | undefined;
134
+ };
135
+ /** Completed assistant turn recovered by the transcript owner, before its next model call. */
136
+ export type MuleResume = {
137
+ readonly message: AssistantMessage;
138
+ readonly tools: readonly MuleToolCheckpoint[];
139
+ };
140
+ /** Run inputs that are identical on the Promise and Effect roots. */
141
+ export type MuleRunSettings = {
142
+ readonly model: Model<Api>;
143
+ /** Override Pi's default `streamSimple`, primarily for explicit providers and tests. */
144
+ readonly stream?: MuleStreamFunction | undefined;
145
+ readonly systemPrompt: string;
146
+ readonly messages: readonly Message[];
147
+ /** Continue a durable, completed assistant turn before requesting another response. */
148
+ readonly resume?: MuleResume | undefined;
149
+ /** Pi stream options. Cancellation is the run's interruption, never a signal here. */
150
+ readonly streamOptions?: Omit<SimpleStreamOptions, "signal"> | undefined;
151
+ /** Maximum silence between provider stream events, in milliseconds. */
152
+ readonly modelStreamIdleTimeoutMs?: number | undefined;
153
+ readonly toolExecution?: "parallel" | "sequential" | undefined;
154
+ /**
155
+ * Pi's bounded retry policy around each assistant call (`retryAssistantCall`).
156
+ * Undefined means no retry, matching pi-ai's own default. Hosts that want
157
+ * Pi coding-agent's numbers pass `piCodingAgentRetryDefaults`.
158
+ */
159
+ readonly retry?: RetryPolicy | undefined;
160
+ /** Provider's bounded Retry-After evidence for the last assistant error. */
161
+ readonly retryAfterMs?: ((message: AssistantMessage) => number | undefined) | undefined;
162
+ };
163
+ /** One observed Pi-retry event around a single assistant call. */
164
+ export type MuleRetryObservation = {
165
+ readonly type: "retry_scheduled";
166
+ readonly run: MuleIdentity;
167
+ readonly turn: MuleIdentity;
168
+ readonly attempt: number;
169
+ readonly maxAttempts: number;
170
+ readonly delayMs: number;
171
+ readonly errorMessage: string;
172
+ } | {
173
+ readonly type: "retry_attempt_started";
174
+ readonly run: MuleIdentity;
175
+ readonly turn: MuleIdentity;
176
+ readonly attempt: number;
177
+ } | {
178
+ readonly type: "retry_finished";
179
+ readonly run: MuleIdentity;
180
+ readonly turn: MuleIdentity;
181
+ readonly attempt: number;
182
+ readonly success: boolean;
183
+ readonly finalError?: string;
184
+ };
185
+ /** Honest terminal result of one Mule invocation. */
186
+ export type MuleRunOutcome = {
187
+ readonly status: "completed" | "cancelled" | "failed";
188
+ readonly run: MuleIdentity;
189
+ readonly terminal: MuleIdentity;
190
+ readonly message: AssistantMessage;
191
+ readonly messages: readonly Message[];
192
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,73 @@
1
+ import type { AssistantMessage, Message, RetryPolicy, Tool, ToolCall } from "@earendil-works/pi-ai";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Stream from "effect/Stream";
4
+ import type { MuleCompletedTurn, MuleError, MuleEvent, MuleRetryObservation, MuleRunOutcome, MuleRunSettings, MuleToolExecutionPart, MuleToolRejection } from "./contract.js";
5
+ export type { Api, AssistantMessage, AssistantMessageEvent, Context, ImageContent, JsonValue, Message, Model, RetryPolicy, SimpleStreamOptions, SystemMessage, TextContent, ThinkingContent, ToolCall, ToolResultMessage, Usage, UserMessage, } from "@earendil-works/pi-ai";
6
+ export { Type } from "typebox";
7
+ export type { MuleRetryObservation } from "./contract.js";
8
+ /**
9
+ * Pi coding-agent's retry defaults (`settings-manager.js` `getRetrySettings()`,
10
+ * `@earendil-works/pi-ai` 0.99.0, the version pinned in the lockfile). Pi-ai's
11
+ * own `RetryPolicy` has no default — an undefined policy means no retry — so
12
+ * a host that wants Pi coding-agent's numbers passes this explicitly.
13
+ */
14
+ export declare const piCodingAgentRetryDefaults: RetryPolicy;
15
+ /**
16
+ * Pi coding-agent's default stream idle timeout (`DEFAULT_HTTP_IDLE_TIMEOUT_MS`,
17
+ * `settings-manager.js`). A host that wants Pi's number for
18
+ * `modelStreamIdleTimeoutMs` passes this explicitly.
19
+ */
20
+ export declare const piHttpIdleTimeoutMs = 300000;
21
+ /** Apply Pi's maintained context-overflow semantics to an exact Pi message. */
22
+ export declare function isContextOverflow(message: AssistantMessage, contextWindow?: number): boolean;
23
+ export type { MuleCompletedTurn, MuleResume, MuleToolCheckpoint, MuleError, MuleEvent, MuleIdentity, MuleIdentityKind, MuleRunOutcome, MuleRunSettings, MuleStreamFunction, MuleToolExecutionPart, MuleToolRejection, MuleToolResult, } from "./contract.js";
24
+ /** A decoded, authorized invocation ready to execute. */
25
+ export type MulePreparedTool = {
26
+ /** Run the already-decoded and authorized tool call. */
27
+ readonly execute: () => Stream.Stream<MuleToolExecutionPart, MuleError>;
28
+ };
29
+ /** Pi tool declaration paired with its Effect-native preparation door. */
30
+ export type MuleTool = {
31
+ readonly descriptor: Tool;
32
+ /** Only read-only or explicitly idempotent tools may opt into crash replay. */
33
+ readonly replay?: "safe" | "unsafe" | undefined;
34
+ readonly executionMode?: "parallel" | "sequential" | undefined;
35
+ /** Decode and authorize one exact Pi tool call before it can execute. */
36
+ readonly prepare: (call: ToolCall) => Effect.Effect<MulePreparedTool, MuleToolRejection>;
37
+ };
38
+ /** Effect-native listener. Completion is part of run settlement. */
39
+ export type MuleListener = (event: MuleEvent) => Effect.Effect<void, MuleError>;
40
+ /**
41
+ * Inputs for one complete Pi model/tool loop. Cancel a run by interrupting its
42
+ * fiber; the run then ends with a `cancelled` `run_end`.
43
+ */
44
+ export type MuleRunInput = MuleRunSettings & {
45
+ /** Rebuild the model system prompt immediately before each provider request. */
46
+ readonly prepareSystemPrompt?: (() => Effect.Effect<string, MuleError>) | undefined;
47
+ readonly tools?: readonly MuleTool[] | undefined;
48
+ /** Resolve a fresh provider credential immediately before each Pi call. */
49
+ readonly resolveApiKey?: ((provider: string) => Effect.Effect<string | undefined, MuleError>) | undefined;
50
+ /** Pull messages that should steer the active run at Pi's safe point. */
51
+ readonly getSteeringMessages?: (() => Effect.Effect<readonly Message[], MuleError>) | undefined;
52
+ /** Pull messages queued until the run would otherwise finish. */
53
+ readonly getFollowUpMessages?: (() => Effect.Effect<readonly Message[], MuleError>) | undefined;
54
+ /**
55
+ * Pi's between-turn point: after a turn's tool results and before the next
56
+ * assistant request of the same run. Return a replacement context, such as
57
+ * one rebuilt after compaction, or `undefined` to keep the current one.
58
+ */
59
+ readonly prepareNextTurn?: ((turn: MuleCompletedTurn) => Effect.Effect<readonly Message[] | undefined, MuleError>) | undefined;
60
+ /** Request a graceful stop after the current Pi turn has fully settled. */
61
+ readonly shouldStopAfterTurn?: ((turn: MuleCompletedTurn) => Effect.Effect<boolean, MuleError>) | undefined;
62
+ /** Effect-native observation sink for Pi's retry policy (`retry`), one call per scheduled or finished retry. */
63
+ readonly onRetry?: ((event: MuleRetryObservation) => Effect.Effect<void, MuleError>) | undefined;
64
+ };
65
+ /** Run Pi's model/tool loop inside Effect without changing Pi's values. */
66
+ export declare function run(input: MuleRunInput, listener?: MuleListener): Effect.Effect<MuleRunOutcome, MuleError>;
67
+ /** Canonical Effect-native Mule interface. */
68
+ export type MuleEffect = {
69
+ /** Run one complete Pi model/tool loop. */
70
+ readonly run: (input: MuleRunInput, listener?: MuleListener) => Effect.Effect<MuleRunOutcome, MuleError>;
71
+ };
72
+ /** Canonical Effect-native Mule value. */
73
+ export declare const muleEffect: MuleEffect;