@fungi.computer/mule 0.2.2 → 0.2.3
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/dist/contract.d.ts +23 -5
- package/dist/effect.d.ts +3 -1
- package/dist/effect.js +160 -94
- package/docs/api/effect.md +3 -1
- package/docs/api/index.md +38 -25
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -80,6 +80,10 @@ export type MuleEvent = {
|
|
|
80
80
|
readonly turn: MuleIdentity;
|
|
81
81
|
readonly message: MuleIdentity;
|
|
82
82
|
readonly value: Message;
|
|
83
|
+
/** Commit the assistant and its retry decision together before observing retry. */
|
|
84
|
+
readonly retry?: Exclude<MuleRetryObservation, {
|
|
85
|
+
type: "retry_attempt_started";
|
|
86
|
+
}> | undefined;
|
|
83
87
|
/** Application payload independent of the completed model message. */
|
|
84
88
|
readonly data?: JsonValue | undefined;
|
|
85
89
|
} | {
|
|
@@ -135,11 +139,24 @@ export type MuleToolCheckpoint = {
|
|
|
135
139
|
/** Present when the transcript message was committed too. */
|
|
136
140
|
readonly message?: ToolResultMessage | undefined;
|
|
137
141
|
};
|
|
138
|
-
/**
|
|
139
|
-
export type MuleResume = {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
/** Transcript-owned continuation at a completed assistant or model retry boundary. */
|
|
143
|
+
export type MuleResume = Readonly<{
|
|
144
|
+
kind: "assistant";
|
|
145
|
+
message: AssistantMessage;
|
|
146
|
+
tools: readonly MuleToolCheckpoint[];
|
|
147
|
+
}> | (Readonly<{
|
|
148
|
+
kind: "retry";
|
|
149
|
+
/** Number of the retry waiting or already dispatched; zero is the initial request. */
|
|
150
|
+
attempt: number;
|
|
151
|
+
/** The previously recorded retry ceiling, when a retry has been scheduled. */
|
|
152
|
+
maxAttempts?: number;
|
|
153
|
+
message?: AssistantMessage;
|
|
154
|
+
}> & (Readonly<{
|
|
155
|
+
phase: "waiting";
|
|
156
|
+
dueAt: number;
|
|
157
|
+
}> | Readonly<{
|
|
158
|
+
phase: "dispatched";
|
|
159
|
+
}>));
|
|
143
160
|
/** Run inputs that are identical on the Promise and Effect roots. */
|
|
144
161
|
export type MuleRunSettings = {
|
|
145
162
|
readonly model: Model<Api>;
|
|
@@ -185,6 +202,7 @@ export type MuleRetryObservation = {
|
|
|
185
202
|
readonly attempt: number;
|
|
186
203
|
readonly maxAttempts: number;
|
|
187
204
|
readonly delayMs: number;
|
|
205
|
+
readonly dueAt: number;
|
|
188
206
|
readonly errorMessage: string;
|
|
189
207
|
} | {
|
|
190
208
|
readonly type: "retry_attempt_started";
|
package/dist/effect.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export { Type } from "typebox";
|
|
|
11
11
|
export type { MuleRetryObservation } from "./contract.js";
|
|
12
12
|
/**
|
|
13
13
|
* Pi coding-agent's retry defaults (`settings-manager.js` `getRetrySettings()`,
|
|
14
|
-
* `@earendil-works/pi-ai`
|
|
14
|
+
* `@earendil-works/pi-ai` 1.3.0, the version pinned in the lockfile). Pi-ai's
|
|
15
15
|
* own `RetryPolicy` has no default — an undefined policy means no retry — so
|
|
16
16
|
* a host that wants Pi coding-agent's numbers passes this explicitly.
|
|
17
17
|
*/
|
|
@@ -40,6 +40,8 @@ export type MuleListener = (event: MuleEvent) => Effect.Effect<void, MuleError>;
|
|
|
40
40
|
* fiber; the run then ends with a `cancelled` `run_end`.
|
|
41
41
|
*/
|
|
42
42
|
export type MuleRunInput = MuleRunSettings & {
|
|
43
|
+
/** Notify the owner when provider cleanup was detached or failed; abort alone is not drain evidence. */
|
|
44
|
+
readonly onUndrained?: Effect.Effect<void> | undefined;
|
|
43
45
|
/** Rebuild the model system prompt immediately before each provider request. */
|
|
44
46
|
readonly prepareSystemPrompt?: (() => Effect.Effect<string, MuleError>) | undefined;
|
|
45
47
|
readonly tools?: readonly MuleTool[] | undefined;
|
package/dist/effect.js
CHANGED
|
@@ -20,7 +20,7 @@ import * as Stream from "effect/Stream";
|
|
|
20
20
|
export { Type } from "typebox";
|
|
21
21
|
/**
|
|
22
22
|
* Pi coding-agent's retry defaults (`settings-manager.js` `getRetrySettings()`,
|
|
23
|
-
* `@earendil-works/pi-ai`
|
|
23
|
+
* `@earendil-works/pi-ai` 1.3.0, the version pinned in the lockfile). Pi-ai's
|
|
24
24
|
* own `RetryPolicy` has no default — an undefined policy means no retry — so
|
|
25
25
|
* a host that wants Pi coding-agent's numbers passes this explicitly.
|
|
26
26
|
*/
|
|
@@ -279,9 +279,35 @@ function streamTurn(input, context, run, turn, emit, attempt) {
|
|
|
279
279
|
return withModelStreamDeadline(effect, Math.max(0, Math.min(wholeRemaining, gapRemaining)), () => limitError(kind).message);
|
|
280
280
|
};
|
|
281
281
|
const controller = new AbortController();
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
282
|
+
const provider = {};
|
|
283
|
+
yield* Effect.addFinalizer(() => Effect.tryPromise({
|
|
284
|
+
try: () => {
|
|
285
|
+
controller.abort("Mule model scope closed");
|
|
286
|
+
if (provider.response === undefined)
|
|
287
|
+
return Promise.resolve(true);
|
|
288
|
+
// Iterator.return only closes the consumer. Pi's result is the
|
|
289
|
+
// producer's terminal, including an abort-aware provider's error.
|
|
290
|
+
const cleanup = provider.response.then(async (response) => {
|
|
291
|
+
await Promise.all([
|
|
292
|
+
provider.iterator?.return?.(),
|
|
293
|
+
response.result(),
|
|
294
|
+
]);
|
|
295
|
+
}, () => undefined);
|
|
296
|
+
return new Promise((resolve) => {
|
|
297
|
+
const timer = setTimeout(() => {
|
|
298
|
+
resolve(false);
|
|
299
|
+
}, providerIteratorReleaseGraceMs);
|
|
300
|
+
cleanup.then(() => {
|
|
301
|
+
clearTimeout(timer);
|
|
302
|
+
resolve(true);
|
|
303
|
+
}, () => {
|
|
304
|
+
clearTimeout(timer);
|
|
305
|
+
resolve(false);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
},
|
|
309
|
+
catch: (cause) => failure("model", "Pi model stream failed", cause),
|
|
310
|
+
}).pipe(Effect.orElseSucceed(() => false), Effect.flatMap((released) => released ? Effect.void : (input.onUndrained ?? Effect.void))));
|
|
285
311
|
const resolvedApiKey = (input.resolveApiKey === undefined
|
|
286
312
|
? undefined
|
|
287
313
|
: yield* input.resolveApiKey(input.model.provider)) ??
|
|
@@ -306,30 +332,15 @@ function streamTurn(input, context, run, turn, emit, attempt) {
|
|
|
306
332
|
if (descriptors.length > 0)
|
|
307
333
|
piContext.tools = descriptors;
|
|
308
334
|
const response = yield* deadline(Effect.tryPromise({
|
|
309
|
-
try:
|
|
335
|
+
try: () => {
|
|
310
336
|
const stream = input.stream ?? streamPi;
|
|
311
|
-
|
|
337
|
+
provider.response = Promise.resolve(stream(input.model, normalizeContext(piContext), options));
|
|
338
|
+
return provider.response;
|
|
312
339
|
},
|
|
313
340
|
catch: (cause) => failure("model", "Pi model stream could not start", cause),
|
|
314
341
|
}));
|
|
315
|
-
const iterator =
|
|
316
|
-
|
|
317
|
-
controller.abort("Mule model stream closed");
|
|
318
|
-
// Provider iterators can ignore abort while a read is pending.
|
|
319
|
-
// Releasing a timed-out run must not await that iterator forever.
|
|
320
|
-
return new Promise((resolve) => {
|
|
321
|
-
const timer = setTimeout(resolve, providerIteratorReleaseGraceMs);
|
|
322
|
-
Promise.resolve(active.return?.()).then(() => {
|
|
323
|
-
clearTimeout(timer);
|
|
324
|
-
resolve();
|
|
325
|
-
}, () => {
|
|
326
|
-
clearTimeout(timer);
|
|
327
|
-
resolve();
|
|
328
|
-
});
|
|
329
|
-
});
|
|
330
|
-
},
|
|
331
|
-
catch: (cause) => failure("model", "Pi model stream failed", cause),
|
|
332
|
-
}).pipe(Effect.ignore));
|
|
342
|
+
const iterator = response[Symbol.asyncIterator]();
|
|
343
|
+
provider.iterator = iterator;
|
|
333
344
|
const observeEvent = (event) => {
|
|
334
345
|
const now = Date.now();
|
|
335
346
|
maxGapMs = Math.max(maxGapMs, now - lastEventAt);
|
|
@@ -409,14 +420,7 @@ function streamTurn(input, context, run, turn, emit, attempt) {
|
|
|
409
420
|
value: completed,
|
|
410
421
|
});
|
|
411
422
|
}
|
|
412
|
-
|
|
413
|
-
type: "message_end",
|
|
414
|
-
run,
|
|
415
|
-
turn,
|
|
416
|
-
message: messageIdentity,
|
|
417
|
-
value: completed,
|
|
418
|
-
});
|
|
419
|
-
return { identity: messageIdentity, message: completed };
|
|
423
|
+
return { message: completed, identity: messageIdentity };
|
|
420
424
|
}).pipe(Effect.catch((error) => {
|
|
421
425
|
if (error.cause !== modelLimitCause)
|
|
422
426
|
return Effect.fail(error);
|
|
@@ -459,13 +463,6 @@ function streamTurn(input, context, run, turn, emit, attempt) {
|
|
|
459
463
|
}
|
|
460
464
|
else
|
|
461
465
|
context[context.length - 1] = value;
|
|
462
|
-
yield* emit({
|
|
463
|
-
type: "message_end",
|
|
464
|
-
run,
|
|
465
|
-
turn,
|
|
466
|
-
message: messageIdentity,
|
|
467
|
-
value,
|
|
468
|
-
});
|
|
469
466
|
return {
|
|
470
467
|
identity: messageIdentity,
|
|
471
468
|
message: value,
|
|
@@ -483,7 +480,7 @@ function streamTurn(input, context, run, turn, emit, attempt) {
|
|
|
483
480
|
* failed attempt's message is removed from `context` before the next
|
|
484
481
|
* attempt runs, so it is never replayed into the model's next call.
|
|
485
482
|
*/
|
|
486
|
-
function streamTurnWithRetry(input, context, run, turn, emit) {
|
|
483
|
+
function streamTurnWithRetry(input, context, run, turn, emit, resume) {
|
|
487
484
|
return Effect.gen(function* () {
|
|
488
485
|
// One turn owns one finite budget, even if its caller updates future policy.
|
|
489
486
|
const policy = input.retry === undefined ? undefined : { ...input.retry };
|
|
@@ -495,10 +492,13 @@ function streamTurnWithRetry(input, context, run, turn, emit) {
|
|
|
495
492
|
].some((value) => !Number.isSafeInteger(value) || value < 0))
|
|
496
493
|
return yield* Effect.fail(failure("model", "Retry policy requires non-negative safe-integer counts and delays"));
|
|
497
494
|
let latest;
|
|
498
|
-
|
|
499
|
-
let
|
|
495
|
+
const maxRetries = Math.min(policy?.maxRetries ?? 0, resume?.maxAttempts ?? Infinity);
|
|
496
|
+
let calls = resume === undefined
|
|
497
|
+
? 0
|
|
498
|
+
: resume.attempt + (resume.phase === "dispatched" ? 1 : 0);
|
|
499
|
+
let retries = resume?.attempt ?? 0;
|
|
500
500
|
let finished = false;
|
|
501
|
-
let lastError;
|
|
501
|
+
let lastError = resume?.message?.errorMessage;
|
|
502
502
|
const reportRetry = (event) => input.onRetry === undefined ? Effect.void : input.onRetry(event);
|
|
503
503
|
const finish = (success, finalError) => Effect.gen(function* () {
|
|
504
504
|
finished = true;
|
|
@@ -513,66 +513,130 @@ function streamTurnWithRetry(input, context, run, turn, emit) {
|
|
|
513
513
|
finalError,
|
|
514
514
|
});
|
|
515
515
|
});
|
|
516
|
+
const waitForRetry = (dueAt) => Effect.gen(function* () {
|
|
517
|
+
yield* Effect.sleep(Math.max(0, dueAt - Date.now()));
|
|
518
|
+
yield* reportRetry({
|
|
519
|
+
type: "retry_attempt_started",
|
|
520
|
+
run,
|
|
521
|
+
turn,
|
|
522
|
+
attempt: retries,
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
const prepareRetry = (errorMessage, after) => Effect.gen(function* () {
|
|
526
|
+
if (policy === undefined)
|
|
527
|
+
return yield* Effect.fail(failure("model", "Retry policy is unavailable"));
|
|
528
|
+
retries++;
|
|
529
|
+
lastError = errorMessage;
|
|
530
|
+
const minimumDelayMs = Math.max(retryDelayMs(policy, retries), after !== undefined &&
|
|
531
|
+
Number.isSafeInteger(after) &&
|
|
532
|
+
after >= 0 &&
|
|
533
|
+
after <= 60_000
|
|
534
|
+
? after
|
|
535
|
+
: 0);
|
|
536
|
+
// Add rather than subtract jitter: a provider's Retry-After is a floor.
|
|
537
|
+
// Jitter stops at one minute; a longer explicit host policy stays intact.
|
|
538
|
+
const jitterMs = Math.min(1_000, minimumDelayMs / 4, Math.max(0, 60_000 - minimumDelayMs));
|
|
539
|
+
const delayMs = minimumDelayMs + Math.floor(Math.random() * jitterMs);
|
|
540
|
+
const dueAt = Date.now() + delayMs;
|
|
541
|
+
return {
|
|
542
|
+
type: "retry_scheduled",
|
|
543
|
+
run,
|
|
544
|
+
turn,
|
|
545
|
+
attempt: retries,
|
|
546
|
+
maxAttempts: maxRetries,
|
|
547
|
+
delayMs,
|
|
548
|
+
dueAt,
|
|
549
|
+
errorMessage: lastError,
|
|
550
|
+
};
|
|
551
|
+
});
|
|
516
552
|
return yield* Effect.gen(function* () {
|
|
553
|
+
if (resume !== undefined) {
|
|
554
|
+
if (!Number.isSafeInteger(resume.attempt) ||
|
|
555
|
+
resume.attempt < 0 ||
|
|
556
|
+
(resume.maxAttempts !== undefined &&
|
|
557
|
+
(!Number.isSafeInteger(resume.maxAttempts) ||
|
|
558
|
+
resume.maxAttempts < 0)) ||
|
|
559
|
+
(resume.phase === "waiting" &&
|
|
560
|
+
(!Number.isSafeInteger(resume.dueAt) || resume.dueAt < 0)))
|
|
561
|
+
return yield* Effect.fail(failure("model", "Invalid model retry continuation"));
|
|
562
|
+
if (!policy?.enabled ||
|
|
563
|
+
retries > maxRetries ||
|
|
564
|
+
(resume.phase === "dispatched" && retries >= maxRetries)) {
|
|
565
|
+
yield* finish(false, "Model retry budget exhausted after restart");
|
|
566
|
+
return resume.message === undefined
|
|
567
|
+
? yield* Effect.fail(failure("model", "Model retry budget exhausted after restart"))
|
|
568
|
+
: { message: resume.message };
|
|
569
|
+
}
|
|
570
|
+
if (resume.phase === "waiting")
|
|
571
|
+
yield* waitForRetry(resume.dueAt);
|
|
572
|
+
else {
|
|
573
|
+
// A restart is not provider backpressure: charge the attempt, resend now.
|
|
574
|
+
const retry = {
|
|
575
|
+
...(yield* prepareRetry("Model request interrupted by a restart")),
|
|
576
|
+
delayMs: 0,
|
|
577
|
+
dueAt: Date.now(),
|
|
578
|
+
};
|
|
579
|
+
yield* reportRetry(retry);
|
|
580
|
+
yield* waitForRetry(retry.dueAt);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
517
583
|
for (;;) {
|
|
518
584
|
if (latest !== undefined) {
|
|
519
585
|
const index = context.indexOf(latest.message);
|
|
520
586
|
if (index !== -1)
|
|
521
587
|
context.splice(index, 1);
|
|
522
588
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
589
|
+
const streamed = yield* streamTurn(input, context, run, turn, emit, ++calls);
|
|
590
|
+
latest = streamed;
|
|
591
|
+
const message = streamed.message;
|
|
592
|
+
let retry;
|
|
593
|
+
if (message.stopReason === "error" &&
|
|
594
|
+
policy?.enabled &&
|
|
595
|
+
retries < maxRetries) {
|
|
596
|
+
const retryable = yield* Effect.try({
|
|
597
|
+
try: () => streamed.limitReached === true || input.retryable(message),
|
|
598
|
+
catch: (cause) => failure("model", "Typed model retry decision failed", cause),
|
|
599
|
+
});
|
|
600
|
+
if (retryable) {
|
|
601
|
+
const after = yield* Effect.try({
|
|
602
|
+
try: () => input.retryAfterMs?.(message),
|
|
603
|
+
catch: (cause) => failure("model", "Model retry delay observation failed", cause),
|
|
604
|
+
});
|
|
605
|
+
retry = yield* prepareRetry(message.errorMessage ?? "Unknown error", after);
|
|
532
606
|
}
|
|
533
|
-
return latest;
|
|
534
607
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
608
|
+
if (retry === undefined &&
|
|
609
|
+
(message.stopReason === "error" || retries > 0)) {
|
|
610
|
+
const terminal = {
|
|
611
|
+
type: "retry_finished",
|
|
612
|
+
run,
|
|
613
|
+
turn,
|
|
614
|
+
attempt: retries,
|
|
615
|
+
success: message.stopReason !== "error" &&
|
|
616
|
+
message.stopReason !== "aborted",
|
|
617
|
+
};
|
|
618
|
+
retry =
|
|
619
|
+
message.stopReason === "error" && message.errorMessage !== undefined
|
|
620
|
+
? { ...terminal, finalError: message.errorMessage }
|
|
621
|
+
: terminal;
|
|
543
622
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
catch: (cause) => failure("model", "Model retry delay observation failed", cause),
|
|
549
|
-
});
|
|
550
|
-
const minimumDelayMs = Math.max(retryDelayMs(policy, retries), after !== undefined &&
|
|
551
|
-
Number.isSafeInteger(after) &&
|
|
552
|
-
after >= 0 &&
|
|
553
|
-
after <= 60_000
|
|
554
|
-
? after
|
|
555
|
-
: 0);
|
|
556
|
-
// Add rather than subtract jitter: a provider's Retry-After is a floor.
|
|
557
|
-
// Jitter stops at one minute; a longer explicit host policy stays intact.
|
|
558
|
-
const jitterMs = Math.min(1_000, minimumDelayMs / 4, Math.max(0, 60_000 - minimumDelayMs));
|
|
559
|
-
const delayMs = minimumDelayMs + Math.floor(Math.random() * jitterMs);
|
|
560
|
-
yield* reportRetry({
|
|
561
|
-
type: "retry_scheduled",
|
|
562
|
-
run,
|
|
563
|
-
turn,
|
|
564
|
-
attempt: retries,
|
|
565
|
-
maxAttempts: policy.maxRetries,
|
|
566
|
-
delayMs,
|
|
567
|
-
errorMessage: lastError,
|
|
568
|
-
});
|
|
569
|
-
yield* Effect.sleep(delayMs);
|
|
570
|
-
yield* reportRetry({
|
|
571
|
-
type: "retry_attempt_started",
|
|
623
|
+
// The recorder receives one decision boundary. No await can expose a
|
|
624
|
+
// completed error without the retry deadline (or terminal decision).
|
|
625
|
+
yield* emit({
|
|
626
|
+
type: "message_end",
|
|
572
627
|
run,
|
|
573
628
|
turn,
|
|
574
|
-
|
|
629
|
+
message: streamed.identity,
|
|
630
|
+
value: message,
|
|
631
|
+
retry,
|
|
575
632
|
});
|
|
633
|
+
if (retry?.type === "retry_finished")
|
|
634
|
+
finished = true;
|
|
635
|
+
if (retry !== undefined && retries > 0)
|
|
636
|
+
yield* reportRetry(retry);
|
|
637
|
+
if (retry?.type !== "retry_scheduled")
|
|
638
|
+
return latest;
|
|
639
|
+
yield* waitForRetry(retry.dueAt);
|
|
576
640
|
}
|
|
577
641
|
}).pipe(Effect.onInterrupt(() => retries === 0 || finished ? Effect.void : finish(false, lastError)));
|
|
578
642
|
});
|
|
@@ -603,11 +667,11 @@ function emitPendingMessages(messages, context, newMessages, run, turn, emit) {
|
|
|
603
667
|
});
|
|
604
668
|
})).pipe(Effect.asVoid);
|
|
605
669
|
}
|
|
606
|
-
function completeLoopTurn(input, context, newMessages, run, turn, pending, emit) {
|
|
670
|
+
function completeLoopTurn(input, context, newMessages, run, turn, pending, emit, retryResume) {
|
|
607
671
|
return Effect.gen(function* () {
|
|
608
672
|
yield* emit({ type: "turn_start", run, turn });
|
|
609
673
|
yield* emitPendingMessages(pending, context, newMessages, run, turn, emit);
|
|
610
|
-
const streamed = yield* streamTurnWithRetry(input, context, run, turn, emit);
|
|
674
|
+
const streamed = yield* streamTurnWithRetry(input, context, run, turn, emit, retryResume);
|
|
611
675
|
// Pi keeps partially streamed tool calls on an aborted or errored message.
|
|
612
676
|
// Like Pi's loop, the turn ends there: nothing executes and no tool result
|
|
613
677
|
// pretends a call ran. The run then settles with the honest terminal.
|
|
@@ -800,7 +864,8 @@ function runLoop(input, emit, runIdentity, terminalIdentity) {
|
|
|
800
864
|
let finalMessage;
|
|
801
865
|
let continueLoop;
|
|
802
866
|
yield* emit({ type: "run_start", run: runIdentity });
|
|
803
|
-
|
|
867
|
+
let retryResume = input.resume?.kind === "retry" ? input.resume : undefined;
|
|
868
|
+
if (input.resume?.kind === "assistant") {
|
|
804
869
|
const resumed = yield* resumeTurn(input, input.resume, context, newMessages, runIdentity, emit);
|
|
805
870
|
const status = statusFor(resumed.message);
|
|
806
871
|
const next = status === "completed"
|
|
@@ -811,7 +876,8 @@ function runLoop(input, emit, runIdentity, terminalIdentity) {
|
|
|
811
876
|
pending.push(...next.pending);
|
|
812
877
|
}
|
|
813
878
|
do {
|
|
814
|
-
const completed = yield* completeLoopTurn(input, context, newMessages, runIdentity, identity("turn", runIdentity), pending, emit);
|
|
879
|
+
const completed = yield* completeLoopTurn(input, context, newMessages, runIdentity, identity("turn", runIdentity), pending, emit, retryResume);
|
|
880
|
+
retryResume = undefined;
|
|
815
881
|
finalMessage = completed.message;
|
|
816
882
|
const status = statusFor(completed.message);
|
|
817
883
|
if (status !== "completed") {
|
package/docs/api/effect.md
CHANGED
|
@@ -21,6 +21,7 @@ type MuleRetryObservation =
|
|
|
21
21
|
attempt: number;
|
|
22
22
|
maxAttempts: number;
|
|
23
23
|
delayMs: number;
|
|
24
|
+
dueAt: number;
|
|
24
25
|
errorMessage: string;
|
|
25
26
|
}
|
|
26
27
|
| {
|
|
@@ -152,6 +153,7 @@ fiber; the run then ends with a `cancelled` `run_end`.
|
|
|
152
153
|
|
|
153
154
|
| Name | Type | Description |
|
|
154
155
|
| ------------------------ | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
156
|
+
| `onUndrained?` | `Effect.Effect`\<`void`\> | Notify the owner when provider cleanup was detached or failed; abort alone is not drain evidence. |
|
|
155
157
|
| `prepareSystemPrompt()?` | () => `Effect.Effect`\<`string`, [`MuleError`](index.md#muleerror)\> | Rebuild the model system prompt immediately before each provider request. |
|
|
156
158
|
| `tools?` | readonly [`MuleTool`](#muletool)[] | - |
|
|
157
159
|
| `resolveApiKey()?` | (`provider`) => `Effect.Effect`\<`string` \| `undefined`, [`MuleError`](index.md#muleerror)\> | Resolve a fresh provider credential immediately before each Pi call. |
|
|
@@ -1849,7 +1851,7 @@ const piCodingAgentRetryDefaults: RetryPolicy;
|
|
|
1849
1851
|
```
|
|
1850
1852
|
|
|
1851
1853
|
Pi coding-agent's retry defaults (`settings-manager.js` `getRetrySettings()`,
|
|
1852
|
-
`@earendil-works/pi-ai`
|
|
1854
|
+
`@earendil-works/pi-ai` 1.3.0, the version pinned in the lockfile). Pi-ai's own
|
|
1853
1855
|
`RetryPolicy` has no default — an undefined policy means no retry — so a host
|
|
1854
1856
|
that wants Pi coding-agent's numbers passes this explicitly.
|
|
1855
1857
|
|
package/docs/api/index.md
CHANGED
|
@@ -281,6 +281,12 @@ type MuleEvent =
|
|
|
281
281
|
turn: MuleIdentity;
|
|
282
282
|
message: MuleIdentity;
|
|
283
283
|
value: Message;
|
|
284
|
+
retry?: Exclude<
|
|
285
|
+
MuleRetryObservation,
|
|
286
|
+
{
|
|
287
|
+
type: "retry_attempt_started";
|
|
288
|
+
}
|
|
289
|
+
>;
|
|
284
290
|
data?: JsonValue;
|
|
285
291
|
}
|
|
286
292
|
| {
|
|
@@ -409,18 +415,22 @@ Lifecycle emitted around Pi values. Pi messages and events are not rewritten.
|
|
|
409
415
|
turn: MuleIdentity;
|
|
410
416
|
message: MuleIdentity;
|
|
411
417
|
value: Message;
|
|
418
|
+
retry?: Exclude<MuleRetryObservation, {
|
|
419
|
+
type: "retry_attempt_started";
|
|
420
|
+
}>;
|
|
412
421
|
data?: JsonValue;
|
|
413
422
|
}
|
|
414
423
|
```
|
|
415
424
|
|
|
416
|
-
| Name | Type
|
|
417
|
-
| --------- |
|
|
418
|
-
| `type` | `"message_end"`
|
|
419
|
-
| `run` | [`MuleIdentity`](#muleidentity)
|
|
420
|
-
| `turn` | [`MuleIdentity`](#muleidentity)
|
|
421
|
-
| `message` | [`MuleIdentity`](#muleidentity)
|
|
422
|
-
| `value` | [`Message`](effect.md#message)
|
|
423
|
-
| `
|
|
425
|
+
| Name | Type | Description |
|
|
426
|
+
| --------- | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
427
|
+
| `type` | `"message_end"` | - |
|
|
428
|
+
| `run` | [`MuleIdentity`](#muleidentity) | - |
|
|
429
|
+
| `turn` | [`MuleIdentity`](#muleidentity) | - |
|
|
430
|
+
| `message` | [`MuleIdentity`](#muleidentity) | - |
|
|
431
|
+
| `value` | [`Message`](effect.md#message) | - |
|
|
432
|
+
| `retry?` | `Exclude`\<[`MuleRetryObservation`](effect.md#muleretryobservation), \{ `type`: `"retry_attempt_started"`; \}\> | Commit the assistant and its retry decision together before observing retry. |
|
|
433
|
+
| `data?` | [`JsonValue`](effect.md#jsonvalue) | Application payload independent of the completed model message. |
|
|
424
434
|
|
|
425
435
|
---
|
|
426
436
|
|
|
@@ -574,25 +584,28 @@ Durable evidence for a call in an interrupted assistant turn.
|
|
|
574
584
|
### MuleResume
|
|
575
585
|
|
|
576
586
|
```ts
|
|
577
|
-
type MuleResume =
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
587
|
+
type MuleResume =
|
|
588
|
+
| Readonly<{
|
|
589
|
+
kind: "assistant";
|
|
590
|
+
message: AssistantMessage;
|
|
591
|
+
tools: readonly MuleToolCheckpoint[];
|
|
592
|
+
}>
|
|
593
|
+
| Readonly<{
|
|
594
|
+
kind: "retry";
|
|
595
|
+
attempt: number;
|
|
596
|
+
maxAttempts?: number;
|
|
597
|
+
message?: AssistantMessage;
|
|
598
|
+
}> &
|
|
599
|
+
| Readonly<{
|
|
600
|
+
phase: "waiting";
|
|
601
|
+
dueAt: number;
|
|
602
|
+
}>
|
|
603
|
+
| Readonly<{
|
|
604
|
+
phase: "dispatched";
|
|
605
|
+
}>;
|
|
589
606
|
```
|
|
590
607
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
```ts
|
|
594
|
-
readonly tools: readonly MuleToolCheckpoint[];
|
|
595
|
-
```
|
|
608
|
+
Transcript-owned continuation at a completed assistant or model retry boundary.
|
|
596
609
|
|
|
597
610
|
---
|
|
598
611
|
|