@fungi.computer/mule 0.1.0 → 0.2.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/THIRD_PARTY_NOTICES +8 -3
- package/dist/contract.d.ts +10 -2
- package/dist/effect.d.ts +5 -8
- package/dist/effect.js +141 -29
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/pi-options.d.ts +6 -0
- package/dist/pi-options.js +35 -0
- package/dist/provider-data.d.ts +3 -0
- package/dist/provider-data.js +17 -0
- package/dist/provider-model.d.ts +3 -0
- package/dist/provider-model.js +18 -0
- package/dist/provider-status.d.ts +1 -1
- package/dist/provider.d.ts +3 -0
- package/dist/provider.js +7 -0
- package/dist/testing.d.ts +47 -4
- package/dist/testing.js +15 -7
- package/dist/types.d.ts +383 -0
- package/dist/types.js +1 -0
- package/docs/api/effect.md +1674 -29
- package/docs/api/index.md +120 -41
- package/docs/api/testing.md +232 -6
- package/package.json +1 -1
package/THIRD_PARTY_NOTICES
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
Mule's direct runtime dependencies are Effect
|
|
2
|
-
@earendil-works/pi-ai
|
|
1
|
+
Mule's direct runtime dependencies are Effect 4.0.2, Cairn, and
|
|
2
|
+
@earendil-works/pi-ai 1.3.0. Consult each dependency's distribution for its
|
|
3
3
|
license terms.
|
|
4
4
|
|
|
5
|
-
The Pi
|
|
5
|
+
The original Pi loop source was pinned from commit
|
|
6
6
|
853a80d26c90a14c1886f0ebb8ffaae133ca2185 and distributed under the MIT
|
|
7
7
|
License, Copyright (c) 2025 Mario Zechner. Its independently archived source
|
|
8
8
|
boundary has SHA-256
|
|
@@ -13,3 +13,8 @@ dfb11f4c1c00e47003db12765371198e944c8b640c995a1dc1cf9130bc9e49ee.
|
|
|
13
13
|
behavior from Botanical's retained @shiit/agent-core 0.66.2 at commit
|
|
14
14
|
b6eabe5dfd3cac9514025d26abbe9a92b083f10b, distributed under the MIT License,
|
|
15
15
|
Copyright (c) 2024 fungi.computer.
|
|
16
|
+
|
|
17
|
+
`src/types.ts` adapts the provider-neutral data declarations from
|
|
18
|
+
@earendil-works/pi-ai 1.3.0, distributed under the MIT License,
|
|
19
|
+
Copyright (c) 2025 Mario Zechner. Mule owns this public structural contract;
|
|
20
|
+
provider implementations remain in Pi.
|
package/dist/contract.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { Api, AssistantMessage, AssistantMessageEvent, AssistantMessageEventStream,
|
|
1
|
+
import type { Api, AssistantMessage, AssistantMessageEvent, AssistantMessageEventStream, Context, ImageContent, JsonValue, Message, Model, RetryPolicy, SimpleStreamOptions, TextContent, ToolCall, ToolResultMessage, Usage } from "./types.js";
|
|
2
2
|
/** Pi's model stream function, supplied explicitly by the caller. */
|
|
3
|
-
export type MuleStreamFunction = (model: Model<Api>, context:
|
|
3
|
+
export type MuleStreamFunction = (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream | Promise<AssistantMessageEventStream>;
|
|
4
4
|
/** Causal roles added around Pi's model and tool loop. */
|
|
5
5
|
export type MuleIdentityKind = "run" | "turn" | "message" | "tool_call" | "tool_result" | "terminal";
|
|
6
6
|
/** One run-local causal identity. */
|
|
@@ -154,6 +154,12 @@ export type MuleRunSettings = {
|
|
|
154
154
|
readonly streamOptions?: Omit<SimpleStreamOptions, "signal" | "maxRetries"> | undefined;
|
|
155
155
|
/** Maximum silence between provider stream events, in milliseconds. */
|
|
156
156
|
readonly modelStreamIdleTimeoutMs?: number | undefined;
|
|
157
|
+
/** Trusted per-attempt safety limits may only tighten Mule's defaults. */
|
|
158
|
+
readonly modelStreamLimits?: Partial<{
|
|
159
|
+
firstEventMs: number;
|
|
160
|
+
attemptMs: number;
|
|
161
|
+
outputBytes: number;
|
|
162
|
+
}> | undefined;
|
|
157
163
|
readonly toolExecution?: "parallel" | "sequential" | undefined;
|
|
158
164
|
/**
|
|
159
165
|
* Bounded retry around each assistant call, using Pi's exponential delay.
|
|
@@ -200,4 +206,6 @@ export type MuleRunOutcome = {
|
|
|
200
206
|
readonly terminal: MuleIdentity;
|
|
201
207
|
readonly message: AssistantMessage;
|
|
202
208
|
readonly messages: readonly Message[];
|
|
209
|
+
/** Mule-owned limit diagnostic, not untrusted provider prose. */
|
|
210
|
+
readonly modelLimitReason?: string;
|
|
203
211
|
};
|
package/dist/effect.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
export { decodeProviderData } from "./provider-data.js";
|
|
2
|
+
export { streamPi as streamModel } from "./provider.js";
|
|
1
3
|
export { observeProviderStatus, type ProviderRejection, type ProviderStatusRecorder, } from "./provider-status.js";
|
|
2
|
-
|
|
4
|
+
export { normalizeModel } from "./provider-model.js";
|
|
5
|
+
import type { AssistantMessage, Message, RetryPolicy, Tool, ToolCall } from "./types.js";
|
|
3
6
|
import * as Effect from "effect/Effect";
|
|
4
7
|
import * as Stream from "effect/Stream";
|
|
5
8
|
import type { MuleCompletedTurn, MuleError, MuleEvent, MuleRetryObservation, MuleRunOutcome, MuleRunSettings, MuleToolExecutionPart, MuleToolRejection } from "./contract.js";
|
|
6
|
-
export type
|
|
9
|
+
export type * from "./types.js";
|
|
7
10
|
export { Type } from "typebox";
|
|
8
11
|
export type { MuleRetryObservation } from "./contract.js";
|
|
9
12
|
/**
|
|
@@ -13,12 +16,6 @@ export type { MuleRetryObservation } from "./contract.js";
|
|
|
13
16
|
* a host that wants Pi coding-agent's numbers passes this explicitly.
|
|
14
17
|
*/
|
|
15
18
|
export declare const piCodingAgentRetryDefaults: RetryPolicy;
|
|
16
|
-
/**
|
|
17
|
-
* Pi coding-agent's default stream idle timeout (`DEFAULT_HTTP_IDLE_TIMEOUT_MS`,
|
|
18
|
-
* `settings-manager.js`). A host that wants Pi's number for
|
|
19
|
-
* `modelStreamIdleTimeoutMs` passes this explicitly.
|
|
20
|
-
*/
|
|
21
|
-
export declare const piHttpIdleTimeoutMs = 300000;
|
|
22
19
|
/** Apply Pi's maintained context-overflow semantics to an exact Pi message. */
|
|
23
20
|
export declare function isContextOverflow(message: AssistantMessage, contextWindow?: number): boolean;
|
|
24
21
|
export type { MuleCompletedTurn, MuleResume, MuleToolCheckpoint, MuleError, MuleEvent, MuleIdentity, MuleIdentityKind, MuleRunOutcome, MuleRunSettings, MuleStreamFunction, MuleToolExecutionPart, MuleToolRejection, MuleToolResult, } from "./contract.js";
|
package/dist/effect.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { normalizeContext } from "@earendil-works/pi-ai/utils/transcript";
|
|
2
|
+
export { decodeProviderData } from "./provider-data.js";
|
|
3
|
+
export { streamPi as streamModel } from "./provider.js";
|
|
1
4
|
import { nanoid } from "@fungi.computer/cairn";
|
|
2
5
|
export { observeProviderStatus, } from "./provider-status.js";
|
|
3
6
|
// Pi's root entry also evaluates its TypeBox validator compiler and model
|
|
@@ -5,7 +8,8 @@ export { observeProviderStatus, } from "./provider-status.js";
|
|
|
5
8
|
// keeps that work out of every host's startup.
|
|
6
9
|
import { isContextOverflow as piIsContextOverflow } from "@earendil-works/pi-ai/utils/overflow";
|
|
7
10
|
import { DEFAULT_MAX_AGENT_RETRY_DELAY_MS, retryDelayMs, } from "@earendil-works/pi-ai/utils/retry";
|
|
8
|
-
import {
|
|
11
|
+
import { streamPi } from "./provider.js";
|
|
12
|
+
export { normalizeModel } from "./provider-model.js";
|
|
9
13
|
import * as Effect from "effect/Effect";
|
|
10
14
|
import * as Cause from "effect/Cause";
|
|
11
15
|
import * as Result from "effect/Result";
|
|
@@ -26,18 +30,20 @@ export const piCodingAgentRetryDefaults = {
|
|
|
26
30
|
baseDelayMs: 2000,
|
|
27
31
|
maxAgentDelayMs: 60_000,
|
|
28
32
|
};
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
/** Starting safety limits for one provider attempt; hosts may tighten them. Not public: the values are starting points, not a contract. */
|
|
34
|
+
const modelStreamLimits = {
|
|
35
|
+
firstEventMs: 60_000,
|
|
36
|
+
idleMs: 120_000,
|
|
37
|
+
attemptMs: 600_000,
|
|
38
|
+
outputBytes: 2 * 1024 * 1024,
|
|
39
|
+
};
|
|
35
40
|
/**
|
|
36
41
|
* Grace period a release waits for a provider iterator that ignores abort,
|
|
37
42
|
* so a timed-out run's scope close never hangs. This is a Mule transport
|
|
38
43
|
* detail with no Pi equivalent, so it stays a constant, not host config.
|
|
39
44
|
*/
|
|
40
45
|
const providerIteratorReleaseGraceMs = 1_000;
|
|
46
|
+
const modelLimitCause = Symbol("Mule model limit");
|
|
41
47
|
/** Apply Pi's maintained context-overflow semantics to an exact Pi message. */
|
|
42
48
|
export function isContextOverflow(message, contextWindow) {
|
|
43
49
|
return piIsContextOverflow(message, contextWindow);
|
|
@@ -231,17 +237,47 @@ function pullPiEvent(iterator, controller) {
|
|
|
231
237
|
catch: (cause) => failure("model", "Pi model stream failed", cause),
|
|
232
238
|
});
|
|
233
239
|
}
|
|
234
|
-
function withModelStreamDeadline(effect, timeoutMs) {
|
|
235
|
-
if (timeoutMs === undefined)
|
|
236
|
-
return effect;
|
|
240
|
+
function withModelStreamDeadline(effect, timeoutMs, reason) {
|
|
237
241
|
return effect.pipe(Effect.timeoutOrElse({
|
|
238
242
|
duration: timeoutMs,
|
|
239
|
-
orElse: () => Effect.fail(failure("model",
|
|
243
|
+
orElse: () => Effect.fail(failure("model", reason(), modelLimitCause)),
|
|
240
244
|
}));
|
|
241
245
|
}
|
|
242
246
|
function streamTurn(input, context, run, turn, emit, attempt) {
|
|
243
247
|
const messageIdentity = identity("message", turn);
|
|
248
|
+
let partial;
|
|
249
|
+
const streamState = { started: false, terminal: undefined };
|
|
244
250
|
return Effect.scoped(Effect.gen(function* () {
|
|
251
|
+
const limits = input.modelStreamLimits;
|
|
252
|
+
const firstEventMs = Math.min(limits?.firstEventMs ?? modelStreamLimits.firstEventMs, modelStreamLimits.firstEventMs);
|
|
253
|
+
const attemptMs = Math.min(limits?.attemptMs ?? modelStreamLimits.attemptMs, modelStreamLimits.attemptMs);
|
|
254
|
+
const outputLimit = Math.min(limits?.outputBytes ?? modelStreamLimits.outputBytes, modelStreamLimits.outputBytes);
|
|
255
|
+
if ([
|
|
256
|
+
firstEventMs,
|
|
257
|
+
attemptMs,
|
|
258
|
+
outputLimit,
|
|
259
|
+
input.modelStreamIdleTimeoutMs ?? 1,
|
|
260
|
+
].some((value) => !Number.isSafeInteger(value) || value <= 0))
|
|
261
|
+
return yield* Effect.fail(failure("model", "Model stream limits must be positive safe integers"));
|
|
262
|
+
const startedAt = Date.now();
|
|
263
|
+
let lastEventAt = startedAt;
|
|
264
|
+
let maxGapMs = 0;
|
|
265
|
+
let outputBytes = 0;
|
|
266
|
+
const encoder = new TextEncoder();
|
|
267
|
+
let hasEvent = false;
|
|
268
|
+
const limitError = (kind) => failure("model", `Model ${kind} after ${String(Date.now() - startedAt)} ms (max provider gap ${String(Math.max(maxGapMs, Date.now() - lastEventAt))} ms, streamed ${String(outputBytes)} bytes)`, modelLimitCause);
|
|
269
|
+
const deadline = (effect) => {
|
|
270
|
+
const elapsed = Date.now() - startedAt;
|
|
271
|
+
const wholeRemaining = attemptMs - elapsed;
|
|
272
|
+
const gapLimit = Math.min(hasEvent ? modelStreamLimits.idleMs : firstEventMs, input.modelStreamIdleTimeoutMs ?? modelStreamLimits.idleMs);
|
|
273
|
+
const gapRemaining = gapLimit - (Date.now() - lastEventAt);
|
|
274
|
+
const kind = wholeRemaining <= gapRemaining
|
|
275
|
+
? "attempt timed out"
|
|
276
|
+
: hasEvent
|
|
277
|
+
? "stream timed out (silence)"
|
|
278
|
+
: "first event timed out";
|
|
279
|
+
return withModelStreamDeadline(effect, Math.max(0, Math.min(wholeRemaining, gapRemaining)), () => limitError(kind).message);
|
|
280
|
+
};
|
|
245
281
|
const controller = new AbortController();
|
|
246
282
|
yield* Effect.addFinalizer(() => Effect.sync(() => {
|
|
247
283
|
controller.abort("Mule model scope closed");
|
|
@@ -269,18 +305,13 @@ function streamTurn(input, context, run, turn, emit, attempt) {
|
|
|
269
305
|
const descriptors = (input.tools ?? []).map((tool) => tool.descriptor);
|
|
270
306
|
if (descriptors.length > 0)
|
|
271
307
|
piContext.tools = descriptors;
|
|
272
|
-
const response = yield*
|
|
308
|
+
const response = yield* deadline(Effect.tryPromise({
|
|
273
309
|
try: async () => {
|
|
274
|
-
const stream = input.stream ??
|
|
275
|
-
(await import("@earendil-works/pi-ai/compat")).streamSimple;
|
|
310
|
+
const stream = input.stream ?? streamPi;
|
|
276
311
|
return stream(input.model, normalizeContext(piContext), options);
|
|
277
312
|
},
|
|
278
313
|
catch: (cause) => failure("model", "Pi model stream could not start", cause),
|
|
279
|
-
})
|
|
280
|
-
const streamState = {
|
|
281
|
-
started: false,
|
|
282
|
-
terminal: undefined,
|
|
283
|
-
};
|
|
314
|
+
}));
|
|
284
315
|
const iterator = yield* Effect.acquireRelease(Effect.sync(() => response[Symbol.asyncIterator]()), (active) => Effect.tryPromise({
|
|
285
316
|
try: () => {
|
|
286
317
|
controller.abort("Mule model stream closed");
|
|
@@ -300,6 +331,19 @@ function streamTurn(input, context, run, turn, emit, attempt) {
|
|
|
300
331
|
catch: (cause) => failure("model", "Pi model stream failed", cause),
|
|
301
332
|
}).pipe(Effect.ignore));
|
|
302
333
|
const observeEvent = (event) => {
|
|
334
|
+
const now = Date.now();
|
|
335
|
+
maxGapMs = Math.max(maxGapMs, now - lastEventAt);
|
|
336
|
+
lastEventAt = now;
|
|
337
|
+
hasEvent = true;
|
|
338
|
+
if (event.type === "text_delta" ||
|
|
339
|
+
event.type === "thinking_delta" ||
|
|
340
|
+
event.type === "toolcall_delta") {
|
|
341
|
+
outputBytes += encoder.encode(event.delta).byteLength;
|
|
342
|
+
if (outputBytes > outputLimit)
|
|
343
|
+
return Effect.fail(limitError("output limit exceeded"));
|
|
344
|
+
}
|
|
345
|
+
if ("partial" in event)
|
|
346
|
+
partial = event.partial;
|
|
303
347
|
switch (event.type) {
|
|
304
348
|
case "start": {
|
|
305
349
|
context.push(event.partial);
|
|
@@ -342,17 +386,17 @@ function streamTurn(input, context, run, turn, emit, attempt) {
|
|
|
342
386
|
}
|
|
343
387
|
}
|
|
344
388
|
};
|
|
345
|
-
const pull = () =>
|
|
389
|
+
const pull = () => deadline(pullPiEvent(iterator, controller));
|
|
346
390
|
let next = yield* pull();
|
|
347
391
|
while (!next.done) {
|
|
348
392
|
yield* observeEvent(next.value);
|
|
349
393
|
next = yield* pull();
|
|
350
394
|
}
|
|
351
395
|
const completed = streamState.terminal ??
|
|
352
|
-
(yield*
|
|
396
|
+
(yield* deadline(Effect.tryPromise({
|
|
353
397
|
try: () => response.result(),
|
|
354
398
|
catch: (cause) => failure("model", "Pi model stream did not settle", cause),
|
|
355
|
-
})
|
|
399
|
+
})));
|
|
356
400
|
if (streamState.started)
|
|
357
401
|
context[context.length - 1] = completed;
|
|
358
402
|
else {
|
|
@@ -373,7 +417,62 @@ function streamTurn(input, context, run, turn, emit, attempt) {
|
|
|
373
417
|
value: completed,
|
|
374
418
|
});
|
|
375
419
|
return { identity: messageIdentity, message: completed };
|
|
376
|
-
}))
|
|
420
|
+
}).pipe(Effect.catch((error) => {
|
|
421
|
+
if (error.cause !== modelLimitCause)
|
|
422
|
+
return Effect.fail(error);
|
|
423
|
+
const value = {
|
|
424
|
+
role: "assistant",
|
|
425
|
+
content: partial?.content ?? [],
|
|
426
|
+
api: input.model.api,
|
|
427
|
+
provider: input.model.provider,
|
|
428
|
+
model: input.model.id,
|
|
429
|
+
usage: partial?.usage ?? {
|
|
430
|
+
input: 0,
|
|
431
|
+
output: 0,
|
|
432
|
+
cacheRead: 0,
|
|
433
|
+
cacheWrite: 0,
|
|
434
|
+
totalTokens: 0,
|
|
435
|
+
cost: {
|
|
436
|
+
input: 0,
|
|
437
|
+
output: 0,
|
|
438
|
+
cacheRead: 0,
|
|
439
|
+
cacheWrite: 0,
|
|
440
|
+
total: 0,
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
stopReason: "error",
|
|
444
|
+
timestamp: Date.now(),
|
|
445
|
+
errorMessage: input.retry?.enabled && attempt > input.retry.maxRetries
|
|
446
|
+
? `The model failed ${String(attempt)} times on this conversation: ${error.message}`
|
|
447
|
+
: error.message,
|
|
448
|
+
};
|
|
449
|
+
return Effect.gen(function* () {
|
|
450
|
+
if (!streamState.started) {
|
|
451
|
+
context.push(value);
|
|
452
|
+
yield* emit({
|
|
453
|
+
type: "message_start",
|
|
454
|
+
run,
|
|
455
|
+
turn,
|
|
456
|
+
message: messageIdentity,
|
|
457
|
+
value,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
else
|
|
461
|
+
context[context.length - 1] = value;
|
|
462
|
+
yield* emit({
|
|
463
|
+
type: "message_end",
|
|
464
|
+
run,
|
|
465
|
+
turn,
|
|
466
|
+
message: messageIdentity,
|
|
467
|
+
value,
|
|
468
|
+
});
|
|
469
|
+
return {
|
|
470
|
+
identity: messageIdentity,
|
|
471
|
+
message: value,
|
|
472
|
+
limitReached: true,
|
|
473
|
+
};
|
|
474
|
+
});
|
|
475
|
+
})));
|
|
377
476
|
}
|
|
378
477
|
/**
|
|
379
478
|
* Retry only the host's typed transient decision, using Pi's maintained delay.
|
|
@@ -427,13 +526,14 @@ function streamTurnWithRetry(input, context, run, turn, emit) {
|
|
|
427
526
|
policy === undefined ||
|
|
428
527
|
!policy.enabled ||
|
|
429
528
|
retries >= policy.maxRetries) {
|
|
430
|
-
if (retries > 0)
|
|
529
|
+
if (retries > 0) {
|
|
431
530
|
yield* finish(message.stopReason !== "error" &&
|
|
432
531
|
message.stopReason !== "aborted", message.stopReason === "error" ? message.errorMessage : undefined);
|
|
532
|
+
}
|
|
433
533
|
return latest;
|
|
434
534
|
}
|
|
435
535
|
const retryable = yield* Effect.try({
|
|
436
|
-
try: () => input.retryable(message),
|
|
536
|
+
try: () => latest?.limitReached === true || input.retryable(message),
|
|
437
537
|
catch: (cause) => failure("model", "Typed model retry decision failed", cause),
|
|
438
538
|
});
|
|
439
539
|
if (!retryable) {
|
|
@@ -526,11 +626,14 @@ function completeLoopTurn(input, context, newMessages, run, turn, pending, emit)
|
|
|
526
626
|
message: streamed.message,
|
|
527
627
|
toolResults: batch.messages,
|
|
528
628
|
});
|
|
529
|
-
|
|
629
|
+
const completed = {
|
|
530
630
|
message: streamed.message,
|
|
531
631
|
toolResults: batch.messages,
|
|
532
632
|
terminate: batch.terminate,
|
|
533
633
|
};
|
|
634
|
+
if (streamed.limitReached && streamed.message.errorMessage !== undefined)
|
|
635
|
+
return { ...completed, modelLimitReason: streamed.message.errorMessage };
|
|
636
|
+
return completed;
|
|
534
637
|
});
|
|
535
638
|
}
|
|
536
639
|
/** Resolve only missing results. A stored and current safe policy are both required. */
|
|
@@ -627,10 +730,19 @@ function selectNextTurn(input, turn, context, newMessages) {
|
|
|
627
730
|
return { continue: followUps.length > 0, pending: followUps };
|
|
628
731
|
});
|
|
629
732
|
}
|
|
630
|
-
function finishRun(run, terminal, message, messages, status, emit) {
|
|
733
|
+
function finishRun(run, terminal, message, messages, status, emit, modelLimitReason) {
|
|
631
734
|
return Effect.gen(function* () {
|
|
632
735
|
yield* emit({ type: "run_end", run, terminal, outcome: status });
|
|
633
|
-
|
|
736
|
+
const outcome = {
|
|
737
|
+
status,
|
|
738
|
+
run,
|
|
739
|
+
terminal,
|
|
740
|
+
message,
|
|
741
|
+
messages,
|
|
742
|
+
};
|
|
743
|
+
if (modelLimitReason !== undefined)
|
|
744
|
+
return { ...outcome, modelLimitReason };
|
|
745
|
+
return outcome;
|
|
634
746
|
});
|
|
635
747
|
}
|
|
636
748
|
/** Run Pi's model/tool loop inside Effect without changing Pi's values. */
|
|
@@ -699,7 +811,7 @@ function runLoop(input, emit, runIdentity, terminalIdentity) {
|
|
|
699
811
|
finalMessage = completed.message;
|
|
700
812
|
const status = statusFor(completed.message);
|
|
701
813
|
if (status !== "completed") {
|
|
702
|
-
return yield* finishRun(runIdentity, terminalIdentity, completed.message, newMessages, status, emit);
|
|
814
|
+
return yield* finishRun(runIdentity, terminalIdentity, completed.message, newMessages, status, emit, completed.modelLimitReason);
|
|
703
815
|
}
|
|
704
816
|
// Tool results always continue the run: this is where Pi checks its
|
|
705
817
|
// context before the next assistant response (Pi 0.84.4+).
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
export { decodeProviderData } from "./provider-data.js";
|
|
2
|
+
export { streamPi as streamModel } from "./provider.js";
|
|
3
|
+
export { normalizeModel } from "./provider-model.js";
|
|
4
|
+
import type { Message, Tool, ToolCall } from "./types.js";
|
|
2
5
|
export { observeProviderStatus, type ProviderRejection, type ProviderStatusRecorder, } from "./provider-status.js";
|
|
3
6
|
import type { MuleCompletedTurn, MuleEvent, MuleRunOutcome, MuleRunSettings, MuleToolExecutionPart, MuleToolRejection } from "./contract.js";
|
|
4
7
|
export type { MuleCompletedTurn, MuleResume, MuleToolCheckpoint, MuleError, MuleEvent, MuleIdentity, MuleIdentityKind, MuleRunOutcome, MuleRunSettings, MuleStreamFunction, MuleToolExecutionPart, MuleToolRejection, MuleToolResult, } from "./contract.js";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export { decodeProviderData } from "./provider-data.js";
|
|
2
|
+
export { streamPi as streamModel } from "./provider.js";
|
|
3
|
+
export { normalizeModel } from "./provider-model.js";
|
|
1
4
|
export { observeProviderStatus, } from "./provider-status.js";
|
|
2
5
|
import * as Cause from "effect/Cause";
|
|
3
6
|
import * as Effect from "effect/Effect";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SimpleStreamOptions as PiOptions } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { SimpleStreamOptions } from "./types.js";
|
|
3
|
+
/** Adapt Pi's parsed payload and event boundary before invoking owned hooks. */
|
|
4
|
+
export declare function piOptions(options: SimpleStreamOptions | undefined): PiOptions | undefined;
|
|
5
|
+
/** Restore the owned options shape when Pi calls a scripted provider factory. */
|
|
6
|
+
export declare function muleOptions(options: PiOptions | undefined): SimpleStreamOptions | undefined;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { decodeProviderData } from "./provider-data.js";
|
|
3
|
+
import { normalizeModel } from "./provider-model.js";
|
|
4
|
+
/** Adapt Pi's parsed payload and event boundary before invoking owned hooks. */
|
|
5
|
+
export function piOptions(options) {
|
|
6
|
+
if (options === undefined)
|
|
7
|
+
return undefined;
|
|
8
|
+
const { onPayload, onProviderStreamEvent, onResponse, ...rest } = options;
|
|
9
|
+
const adapted = { ...rest };
|
|
10
|
+
if (onPayload !== undefined)
|
|
11
|
+
adapted.onPayload = (payload, model) => onPayload(decodeProviderData(payload), normalizeModel(model));
|
|
12
|
+
if (onProviderStreamEvent !== undefined)
|
|
13
|
+
adapted.onProviderStreamEvent = (event, model) => onProviderStreamEvent(decodeProviderData(event), normalizeModel(model));
|
|
14
|
+
if (onResponse !== undefined)
|
|
15
|
+
adapted.onResponse = (response, model) => onResponse(response, normalizeModel(model));
|
|
16
|
+
return adapted;
|
|
17
|
+
}
|
|
18
|
+
/** Restore the owned options shape when Pi calls a scripted provider factory. */
|
|
19
|
+
export function muleOptions(options) {
|
|
20
|
+
if (options === undefined)
|
|
21
|
+
return undefined;
|
|
22
|
+
const { samplingParams, metadata, onPayload, ...rest } = options;
|
|
23
|
+
const adapted = { ...rest };
|
|
24
|
+
const decodeObject = Schema.decodeUnknownSync(Schema.Record(Schema.String, Schema.Json));
|
|
25
|
+
if (samplingParams !== undefined)
|
|
26
|
+
adapted.samplingParams = decodeObject(samplingParams);
|
|
27
|
+
if (metadata !== undefined)
|
|
28
|
+
adapted.metadata = decodeObject(metadata);
|
|
29
|
+
if (onPayload !== undefined)
|
|
30
|
+
adapted.onPayload = async (payload, model) => {
|
|
31
|
+
const result = await onPayload(payload, model);
|
|
32
|
+
return result === undefined ? undefined : decodeProviderData(result);
|
|
33
|
+
};
|
|
34
|
+
return adapted;
|
|
35
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
const providerValue = Schema.suspend(() => Schema.Union([
|
|
3
|
+
Schema.Undefined,
|
|
4
|
+
Schema.Null,
|
|
5
|
+
Schema.Boolean,
|
|
6
|
+
Schema.Number,
|
|
7
|
+
Schema.String,
|
|
8
|
+
Schema.Uint8Array,
|
|
9
|
+
Schema.instanceOf(AbortSignal),
|
|
10
|
+
Schema.Array(providerValue),
|
|
11
|
+
providerData,
|
|
12
|
+
]));
|
|
13
|
+
const providerData = Schema.Record(Schema.String, providerValue);
|
|
14
|
+
/** Decode Pi's parsed request/event data before it enters an owned hook. */
|
|
15
|
+
export function decodeProviderData(data) {
|
|
16
|
+
return Schema.decodeUnknownSync(providerData)(data);
|
|
17
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { Api, Model } from "./types.js";
|
|
2
|
+
/** Validate a catalog model's opaque sampling fields before it enters Mule. */
|
|
3
|
+
export declare function normalizeModel<TApi extends Api>(model: Omit<Model<TApi>, "samplingParams" | "samplingParamsByThinkingLevel">): Model<TApi>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
const sampling = Schema.Record(Schema.String, Schema.Json);
|
|
3
|
+
const decodeSampling = Schema.decodeUnknownSync(Schema.Struct({
|
|
4
|
+
samplingParams: Schema.optionalKey(sampling),
|
|
5
|
+
samplingParamsByThinkingLevel: Schema.optionalKey(Schema.Struct({
|
|
6
|
+
off: Schema.optionalKey(sampling),
|
|
7
|
+
minimal: Schema.optionalKey(sampling),
|
|
8
|
+
low: Schema.optionalKey(sampling),
|
|
9
|
+
medium: Schema.optionalKey(sampling),
|
|
10
|
+
high: Schema.optionalKey(sampling),
|
|
11
|
+
xhigh: Schema.optionalKey(sampling),
|
|
12
|
+
max: Schema.optionalKey(sampling),
|
|
13
|
+
})),
|
|
14
|
+
}));
|
|
15
|
+
/** Validate a catalog model's opaque sampling fields before it enters Mule. */
|
|
16
|
+
export function normalizeModel(model) {
|
|
17
|
+
return { ...model, ...decodeSampling(model) };
|
|
18
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SimpleStreamOptions } from "
|
|
1
|
+
import type { SimpleStreamOptions } from "./types.js";
|
|
2
2
|
/** Bounded provider evidence, never response bodies, headers or error prose. */
|
|
3
3
|
export type ProviderRejection = Readonly<{
|
|
4
4
|
code: "usage-limit-reached";
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { normalizeContext } from "@earendil-works/pi-ai/utils/transcript";
|
|
2
|
+
import { piOptions } from "./pi-options.js";
|
|
3
|
+
/** Pi's maintained stream behind Mule's structural provider port. */
|
|
4
|
+
export const streamPi = async (model, context, options) => {
|
|
5
|
+
const { streamSimple } = await import("@earendil-works/pi-ai/compat");
|
|
6
|
+
return streamSimple(model, normalizeContext(context), piOptions(options));
|
|
7
|
+
};
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,17 +1,60 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
1
|
+
import type { Api, AssistantMessage, AssistantMessageEventStream, Context, Model } from "./types.js";
|
|
2
|
+
import type { MuleStreamFunction } from "./contract.js";
|
|
3
|
+
/** Credential-free model definition for the scripted provider. */
|
|
4
|
+
export type FauxModelDefinition = {
|
|
5
|
+
id: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
reasoning?: boolean;
|
|
8
|
+
input?: ("text" | "image")[];
|
|
9
|
+
inputLimits?: Model<Api>["inputLimits"];
|
|
10
|
+
cost?: {
|
|
11
|
+
input: number;
|
|
12
|
+
output: number;
|
|
13
|
+
cacheRead: number;
|
|
14
|
+
cacheWrite: number;
|
|
15
|
+
};
|
|
16
|
+
contextWindow?: number;
|
|
17
|
+
maxTokens?: number;
|
|
18
|
+
};
|
|
19
|
+
/** Scripted provider configuration, independent of provider SDK declarations. */
|
|
20
|
+
export type RegisterFauxProviderOptions = {
|
|
21
|
+
api?: string;
|
|
22
|
+
provider?: string;
|
|
23
|
+
models?: FauxModelDefinition[];
|
|
24
|
+
deferred?: {
|
|
25
|
+
pendingFetches?: number;
|
|
26
|
+
pollAfterMs?: number;
|
|
27
|
+
};
|
|
28
|
+
tokensPerSecond?: number;
|
|
29
|
+
tokenSize?: {
|
|
30
|
+
min?: number;
|
|
31
|
+
max?: number;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
/** Counters retained by the scripted provider. */
|
|
35
|
+
export type FauxProviderState = {
|
|
36
|
+
callCount: number;
|
|
37
|
+
deferredFetchCount: number;
|
|
38
|
+
cancelledDeferred: NonNullable<AssistantMessage["deferred"]>[];
|
|
39
|
+
};
|
|
40
|
+
/** Function that observes a model request and supplies a scripted reply. */
|
|
41
|
+
export type FauxResponseFactory = (context: Context, options: Parameters<MuleStreamFunction>[2], state: FauxProviderState, model: Model<Api>) => AssistantMessage | Promise<AssistantMessage>;
|
|
42
|
+
/** A scripted reply or a factory that observes the normalized request. */
|
|
43
|
+
export type FauxResponseStep = AssistantMessage | FauxResponseFactory;
|
|
3
44
|
/** Deterministic model and stream with Pi's observable response state. */
|
|
4
45
|
export type FauxModel = Readonly<{
|
|
5
46
|
/** Model supplied to Mule's run input. */
|
|
6
47
|
model: Model<Api>;
|
|
7
48
|
/** Bound Pi faux stream; never calls a remote provider. */
|
|
8
|
-
stream:
|
|
49
|
+
stream: (...args: Parameters<MuleStreamFunction>) => AssistantMessageEventStream;
|
|
9
50
|
/** Scripted replies have no typed provider failure; opt into retry explicitly. */
|
|
10
51
|
retryable: (message: AssistantMessage) => boolean;
|
|
11
52
|
/** Replace the responses consumed by subsequent assistant calls. */
|
|
12
53
|
setResponses: (replies: readonly FauxResponseStep[]) => void;
|
|
13
54
|
/** Pi's call history and counters for deterministic assertions. */
|
|
14
|
-
state:
|
|
55
|
+
state: FauxProviderState;
|
|
56
|
+
/** Scripted replies not yet consumed, supplied by Pi's maintained queue. */
|
|
57
|
+
getPendingResponseCount: () => number;
|
|
15
58
|
}>;
|
|
16
59
|
/** Create a credential-free model for tests and runnable examples. */
|
|
17
60
|
export declare function fauxModel(replies: readonly FauxResponseStep[], options?: RegisterFauxProviderOptions): FauxModel;
|
package/dist/testing.js
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isFunction } from "effect/Predicate";
|
|
2
|
+
import { muleOptions, piOptions } from "./pi-options.js";
|
|
3
|
+
import { normalizeModel } from "./provider-model.js";
|
|
4
|
+
import { fauxProvider } from "@earendil-works/pi-ai/providers/faux";
|
|
5
|
+
import { normalizeContext } from "@earendil-works/pi-ai/utils/transcript";
|
|
2
6
|
/** Create a credential-free model for tests and runnable examples. */
|
|
3
7
|
export function fauxModel(replies, options = {}) {
|
|
4
8
|
const faux = fauxProvider(options);
|
|
5
|
-
|
|
9
|
+
const setResponses = (responses) => {
|
|
10
|
+
faux.setResponses(responses.map((step) => isFunction(step)
|
|
11
|
+
? (context, options, state, model) => step(context, muleOptions(options), state, normalizeModel(model))
|
|
12
|
+
: step));
|
|
13
|
+
};
|
|
14
|
+
setResponses(replies);
|
|
6
15
|
return {
|
|
7
|
-
model: faux.getModel(),
|
|
8
|
-
stream: faux.provider.streamSimple
|
|
16
|
+
model: normalizeModel(faux.getModel()),
|
|
17
|
+
stream: (model, context, streamOptions) => faux.provider.streamSimple(model, normalizeContext(context), piOptions(streamOptions)),
|
|
9
18
|
retryable: () => false,
|
|
10
|
-
setResponses
|
|
11
|
-
faux.setResponses([...responses]);
|
|
12
|
-
},
|
|
19
|
+
setResponses,
|
|
13
20
|
state: faux.state,
|
|
21
|
+
getPendingResponseCount: () => faux.getPendingResponseCount(),
|
|
14
22
|
};
|
|
15
23
|
}
|