@pencil-agent/nano-pencil 1.14.5 → 1.14.6
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/build-meta.json +3 -3
- package/dist/node_modules/@pencil-agent/agent-core/agent-loop-stream-events.d.ts +18 -0
- package/dist/node_modules/@pencil-agent/agent-core/agent-loop-stream-events.js +55 -0
- package/dist/node_modules/@pencil-agent/agent-core/agent-loop.js +136 -14
- package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-agent-loop.js +107 -14
- package/dist/node_modules/@pencil-agent/ai/stream.js +90 -16
- package/package.json +1 -1
package/dist/build-meta.json
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [WHO]: Provides waitForAbortableOperation(), waitForAssistantStream(), waitForAssistantStreamEvent()
|
|
3
|
+
* [FROM]: Depends on @pencil-agent/ai AssistantMessageEvent/AssistantMessageEventStream contracts.
|
|
4
|
+
* [TO]: Consumed by standard and structured-adaptive agent loops.
|
|
5
|
+
* [HERE]: packages/agent-core/src/agent-loop-stream-events.ts within agent-core; shared abortable operation and assistant-stream iterator utilities.
|
|
6
|
+
*/
|
|
7
|
+
import type { AssistantMessageEvent, AssistantMessageEventStream } from "@pencil-agent/ai";
|
|
8
|
+
export type AssistantStreamNext = IteratorResult<AssistantMessageEvent> | "aborted";
|
|
9
|
+
export type AssistantStreamStart = AssistantMessageEventStream | "aborted";
|
|
10
|
+
export type AbortableOperationResult<T> = {
|
|
11
|
+
type: "resolved";
|
|
12
|
+
value: T;
|
|
13
|
+
} | {
|
|
14
|
+
type: "aborted";
|
|
15
|
+
};
|
|
16
|
+
export declare function waitForAbortableOperation<T>(valueOrPromise: T | Promise<T>, signal?: AbortSignal): Promise<AbortableOperationResult<T>>;
|
|
17
|
+
export declare function waitForAssistantStream(streamOrPromise: AssistantMessageEventStream | Promise<AssistantMessageEventStream>, signal?: AbortSignal): Promise<AssistantStreamStart>;
|
|
18
|
+
export declare function waitForAssistantStreamEvent(iterator: AsyncIterator<AssistantMessageEvent>, signal?: AbortSignal): Promise<AssistantStreamNext>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [WHO]: Provides waitForAbortableOperation(), waitForAssistantStream(), waitForAssistantStreamEvent()
|
|
3
|
+
* [FROM]: Depends on @pencil-agent/ai AssistantMessageEvent/AssistantMessageEventStream contracts.
|
|
4
|
+
* [TO]: Consumed by standard and structured-adaptive agent loops.
|
|
5
|
+
* [HERE]: packages/agent-core/src/agent-loop-stream-events.ts within agent-core; shared abortable operation and assistant-stream iterator utilities.
|
|
6
|
+
*/
|
|
7
|
+
export function waitForAbortableOperation(valueOrPromise, signal) {
|
|
8
|
+
if (signal?.aborted)
|
|
9
|
+
return Promise.resolve({ type: "aborted" });
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const cleanup = () => {
|
|
12
|
+
signal?.removeEventListener("abort", onAbort);
|
|
13
|
+
};
|
|
14
|
+
const onAbort = () => {
|
|
15
|
+
cleanup();
|
|
16
|
+
resolve({ type: "aborted" });
|
|
17
|
+
};
|
|
18
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
19
|
+
Promise.resolve(valueOrPromise).then((value) => {
|
|
20
|
+
cleanup();
|
|
21
|
+
resolve({ type: "resolved", value });
|
|
22
|
+
}, (error) => {
|
|
23
|
+
cleanup();
|
|
24
|
+
reject(error);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export function waitForAssistantStream(streamOrPromise, signal) {
|
|
29
|
+
return waitForAbortableOperation(streamOrPromise, signal).then((result) => {
|
|
30
|
+
if (result.type === "aborted")
|
|
31
|
+
return "aborted";
|
|
32
|
+
return result.value;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export function waitForAssistantStreamEvent(iterator, signal) {
|
|
36
|
+
if (signal?.aborted)
|
|
37
|
+
return Promise.resolve("aborted");
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const cleanup = () => {
|
|
40
|
+
signal?.removeEventListener("abort", onAbort);
|
|
41
|
+
};
|
|
42
|
+
const onAbort = () => {
|
|
43
|
+
cleanup();
|
|
44
|
+
resolve("aborted");
|
|
45
|
+
};
|
|
46
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
47
|
+
iterator.next().then((result) => {
|
|
48
|
+
cleanup();
|
|
49
|
+
resolve(result);
|
|
50
|
+
}, (error) => {
|
|
51
|
+
cleanup();
|
|
52
|
+
reject(error);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -14,6 +14,7 @@ import { computeRecoveryMaxTokens, createOutputTokenRecoveryMessage, createToken
|
|
|
14
14
|
import { createInterruptedToolResults, createSkippedToolCallLimitResults, enforceToolResultBatchSize, } from "./agent-loop-tool-results.js";
|
|
15
15
|
import { flushReadyToolUseSummaries, startToolUseSummary, } from "./agent-loop-tool-summaries.js";
|
|
16
16
|
import { buildAgentRunPolicy, resolveAgentRunLoopFramework } from "./agent-run-result.js";
|
|
17
|
+
import { waitForAbortableOperation, waitForAssistantStream, waitForAssistantStreamEvent, } from "./agent-loop-stream-events.js";
|
|
17
18
|
const DEFAULT_MAX_TURNS_PER_PROMPT = 64;
|
|
18
19
|
const DEFAULT_MAX_TOOL_CALLS_PER_PROMPT = 128;
|
|
19
20
|
const DEFAULT_MAX_STOP_HOOK_CONTINUATIONS = 3;
|
|
@@ -150,7 +151,21 @@ async function runLoop(currentContext, newMessages, config, signal, stream, stre
|
|
|
150
151
|
let stopHookActive = false;
|
|
151
152
|
let stopHookContinuationCount = 0;
|
|
152
153
|
// Check for steering messages at start (user may have typed while waiting)
|
|
153
|
-
|
|
154
|
+
const initialSteeringMessages = await waitForAbortableOperation(config.getSteeringMessages ? config.getSteeringMessages() : [], signal);
|
|
155
|
+
if (initialSteeringMessages.type === "aborted") {
|
|
156
|
+
finishStandardLoopWithAbortedTurn(stream, currentContext, newMessages, {
|
|
157
|
+
config,
|
|
158
|
+
turnCount,
|
|
159
|
+
toolCallCount,
|
|
160
|
+
startedAt,
|
|
161
|
+
usage,
|
|
162
|
+
permissionDenials,
|
|
163
|
+
transitions,
|
|
164
|
+
lastTransition,
|
|
165
|
+
});
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
let pendingMessages = initialSteeringMessages.value || [];
|
|
154
169
|
// Outer loop: continues when queued follow-up messages arrive after agent would stop
|
|
155
170
|
while (true) {
|
|
156
171
|
let hasMoreToolCalls = true;
|
|
@@ -341,12 +356,26 @@ async function runLoop(currentContext, newMessages, config, signal, stream, stre
|
|
|
341
356
|
}
|
|
342
357
|
if (!hasMoreToolCalls && config.runStopHooks && !stopHookActive) {
|
|
343
358
|
stopHookActive = true;
|
|
344
|
-
const stopHookResult = await config.runStopHooks({
|
|
359
|
+
const stopHookResult = await waitForAbortableOperation(config.runStopHooks({
|
|
345
360
|
message,
|
|
346
361
|
messages: currentContext.messages,
|
|
347
|
-
});
|
|
362
|
+
}), signal);
|
|
348
363
|
stopHookActive = false;
|
|
349
|
-
if (stopHookResult.
|
|
364
|
+
if (stopHookResult.type === "aborted") {
|
|
365
|
+
finishStandardLoopWithAbortedTurn(stream, currentContext, newMessages, {
|
|
366
|
+
config,
|
|
367
|
+
turnCount,
|
|
368
|
+
toolCallCount,
|
|
369
|
+
startedAt,
|
|
370
|
+
usage,
|
|
371
|
+
permissionDenials,
|
|
372
|
+
transitions,
|
|
373
|
+
lastTransition,
|
|
374
|
+
});
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const resolvedStopHookResult = stopHookResult.value;
|
|
378
|
+
if (resolvedStopHookResult.action === "continue" && resolvedStopHookResult.messages.length > 0) {
|
|
350
379
|
if (stopHookContinuationCount >= maxStopHookContinuations) {
|
|
351
380
|
const limitMessage = createLoopLimitMessage(config, `stop_hook_limit_reached: stopped after ${maxStopHookContinuations} stop-hook continuation turns.`);
|
|
352
381
|
currentContext.messages.push(limitMessage);
|
|
@@ -374,7 +403,7 @@ async function runLoop(currentContext, newMessages, config, signal, stream, stre
|
|
|
374
403
|
return;
|
|
375
404
|
}
|
|
376
405
|
stopHookContinuationCount += 1;
|
|
377
|
-
pendingMessages =
|
|
406
|
+
pendingMessages = resolvedStopHookResult.messages;
|
|
378
407
|
recordTransition({
|
|
379
408
|
reason: "stop_hook_blocking",
|
|
380
409
|
continuationCount: stopHookContinuationCount,
|
|
@@ -406,7 +435,21 @@ async function runLoop(currentContext, newMessages, config, signal, stream, stre
|
|
|
406
435
|
}
|
|
407
436
|
}
|
|
408
437
|
// Agent would stop here. Check for follow-up messages.
|
|
409
|
-
const
|
|
438
|
+
const followUpMessagesResult = await waitForAbortableOperation(config.getFollowUpMessages ? config.getFollowUpMessages() : [], signal);
|
|
439
|
+
if (followUpMessagesResult.type === "aborted") {
|
|
440
|
+
finishStandardLoopWithAbortedTurn(stream, currentContext, newMessages, {
|
|
441
|
+
config,
|
|
442
|
+
turnCount,
|
|
443
|
+
toolCallCount,
|
|
444
|
+
startedAt,
|
|
445
|
+
usage,
|
|
446
|
+
permissionDenials,
|
|
447
|
+
transitions,
|
|
448
|
+
lastTransition,
|
|
449
|
+
});
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const followUpMessages = followUpMessagesResult.value || [];
|
|
410
453
|
if (followUpMessages.length > 0) {
|
|
411
454
|
// Set as pending so inner loop processes them
|
|
412
455
|
pendingMessages = followUpMessages;
|
|
@@ -436,10 +479,18 @@ async function streamAssistantResponse(context, config, signal, stream, streamFn
|
|
|
436
479
|
// Apply context transform if configured (AgentMessage[] → AgentMessage[])
|
|
437
480
|
let messages = context.messages;
|
|
438
481
|
if (config.transformContext) {
|
|
439
|
-
|
|
482
|
+
const transformedMessages = await waitForAbortableOperation(config.transformContext(messages, signal), signal);
|
|
483
|
+
if (transformedMessages.type === "aborted") {
|
|
484
|
+
return pushAbortedAssistantMessage(context, stream, config);
|
|
485
|
+
}
|
|
486
|
+
messages = transformedMessages.value;
|
|
440
487
|
}
|
|
441
488
|
// Convert to LLM-compatible messages (AgentMessage[] → Message[])
|
|
442
|
-
const
|
|
489
|
+
const convertedMessages = await waitForAbortableOperation(config.convertToLlm(messages), signal);
|
|
490
|
+
if (convertedMessages.type === "aborted") {
|
|
491
|
+
return pushAbortedAssistantMessage(context, stream, config);
|
|
492
|
+
}
|
|
493
|
+
const llmMessages = convertedMessages.value;
|
|
443
494
|
// Build LLM context
|
|
444
495
|
const llmContext = {
|
|
445
496
|
systemPrompt: context.systemPrompt,
|
|
@@ -448,7 +499,11 @@ async function streamAssistantResponse(context, config, signal, stream, streamFn
|
|
|
448
499
|
};
|
|
449
500
|
const streamFunction = streamFn || streamSimple;
|
|
450
501
|
// Resolve API key (important for expiring tokens)
|
|
451
|
-
const
|
|
502
|
+
const apiKeyResult = await waitForAbortableOperation(config.getApiKey ? config.getApiKey(config.model.provider) : undefined, signal);
|
|
503
|
+
if (apiKeyResult.type === "aborted") {
|
|
504
|
+
return pushAbortedAssistantMessage(context, stream, config);
|
|
505
|
+
}
|
|
506
|
+
const resolvedApiKey = apiKeyResult.value || config.apiKey;
|
|
452
507
|
stream.push({
|
|
453
508
|
type: "stream_request_start",
|
|
454
509
|
model: config.model.id,
|
|
@@ -457,15 +512,58 @@ async function streamAssistantResponse(context, config, signal, stream, streamFn
|
|
|
457
512
|
messageCount: llmMessages.length,
|
|
458
513
|
maxTokens: maxTokensOverride ?? config.maxTokens,
|
|
459
514
|
});
|
|
460
|
-
const response = await streamFunction(config.model, llmContext, {
|
|
515
|
+
const response = await waitForAssistantStream(streamFunction(config.model, llmContext, {
|
|
461
516
|
...config,
|
|
462
517
|
maxTokens: maxTokensOverride ?? config.maxTokens,
|
|
463
518
|
apiKey: resolvedApiKey,
|
|
464
519
|
signal,
|
|
465
|
-
});
|
|
520
|
+
}), signal);
|
|
521
|
+
if (response === "aborted") {
|
|
522
|
+
const finalMessage = createLoopLimitMessage(config, "Request was aborted");
|
|
523
|
+
finalMessage.stopReason = "aborted";
|
|
524
|
+
context.messages.push(finalMessage);
|
|
525
|
+
stream.push({ type: "message_start", message: { ...finalMessage } });
|
|
526
|
+
stream.push({ type: "message_end", message: finalMessage });
|
|
527
|
+
return finalMessage;
|
|
528
|
+
}
|
|
466
529
|
let partialMessage = null;
|
|
467
530
|
let addedPartial = false;
|
|
468
|
-
|
|
531
|
+
const responseIterator = response[Symbol.asyncIterator]();
|
|
532
|
+
while (true) {
|
|
533
|
+
let nextEvent;
|
|
534
|
+
try {
|
|
535
|
+
nextEvent = await waitForAssistantStreamEvent(responseIterator, signal);
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
const finalMessage = createLoopLimitMessage(config, error instanceof Error ? error.message : String(error));
|
|
539
|
+
if (addedPartial) {
|
|
540
|
+
context.messages[context.messages.length - 1] = finalMessage;
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
context.messages.push(finalMessage);
|
|
544
|
+
stream.push({ type: "message_start", message: { ...finalMessage } });
|
|
545
|
+
}
|
|
546
|
+
stream.push({ type: "message_end", message: finalMessage });
|
|
547
|
+
return finalMessage;
|
|
548
|
+
}
|
|
549
|
+
if (nextEvent === "aborted") {
|
|
550
|
+
void responseIterator.return?.();
|
|
551
|
+
const finalMessage = createLoopLimitMessage(config, "Request was aborted");
|
|
552
|
+
finalMessage.stopReason = "aborted";
|
|
553
|
+
if (addedPartial) {
|
|
554
|
+
context.messages[context.messages.length - 1] = finalMessage;
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
context.messages.push(finalMessage);
|
|
558
|
+
stream.push({ type: "message_start", message: { ...finalMessage } });
|
|
559
|
+
}
|
|
560
|
+
stream.push({ type: "message_end", message: finalMessage });
|
|
561
|
+
return finalMessage;
|
|
562
|
+
}
|
|
563
|
+
if (nextEvent.done) {
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
const event = nextEvent.value;
|
|
469
567
|
switch (event.type) {
|
|
470
568
|
case "start":
|
|
471
569
|
partialMessage = event.partial;
|
|
@@ -494,7 +592,7 @@ async function streamAssistantResponse(context, config, signal, stream, streamFn
|
|
|
494
592
|
break;
|
|
495
593
|
case "done":
|
|
496
594
|
case "error": {
|
|
497
|
-
const finalMessage =
|
|
595
|
+
const finalMessage = event.type === "done" ? event.message : event.error;
|
|
498
596
|
if (addedPartial) {
|
|
499
597
|
context.messages[context.messages.length - 1] = finalMessage;
|
|
500
598
|
}
|
|
@@ -509,7 +607,8 @@ async function streamAssistantResponse(context, config, signal, stream, streamFn
|
|
|
509
607
|
}
|
|
510
608
|
}
|
|
511
609
|
}
|
|
512
|
-
const finalMessage =
|
|
610
|
+
const finalMessage = response.resultIfResolved() ??
|
|
611
|
+
createLoopLimitMessage(config, "Provider stream ended without a final assistant message");
|
|
513
612
|
if (addedPartial) {
|
|
514
613
|
context.messages[context.messages.length - 1] = finalMessage;
|
|
515
614
|
}
|
|
@@ -520,6 +619,29 @@ async function streamAssistantResponse(context, config, signal, stream, streamFn
|
|
|
520
619
|
stream.push({ type: "message_end", message: finalMessage });
|
|
521
620
|
return finalMessage;
|
|
522
621
|
}
|
|
622
|
+
function pushAbortedAssistantMessage(context, stream, config) {
|
|
623
|
+
const finalMessage = createLoopLimitMessage(config, "Request was aborted");
|
|
624
|
+
finalMessage.stopReason = "aborted";
|
|
625
|
+
context.messages.push(finalMessage);
|
|
626
|
+
stream.push({ type: "message_start", message: { ...finalMessage } });
|
|
627
|
+
stream.push({ type: "message_end", message: finalMessage });
|
|
628
|
+
return finalMessage;
|
|
629
|
+
}
|
|
630
|
+
function finishStandardLoopWithAbortedTurn(stream, context, newMessages, options) {
|
|
631
|
+
const finalMessage = createLoopLimitMessage(options.config, "Request was aborted");
|
|
632
|
+
finalMessage.stopReason = "aborted";
|
|
633
|
+
context.messages.push(finalMessage);
|
|
634
|
+
newMessages.push(finalMessage);
|
|
635
|
+
stream.push({ type: "message_start", message: { ...finalMessage } });
|
|
636
|
+
stream.push({ type: "message_end", message: finalMessage });
|
|
637
|
+
stream.push({ type: "turn_end", message: finalMessage, toolResults: [] });
|
|
638
|
+
finishStandardLoop(stream, newMessages, {
|
|
639
|
+
...options,
|
|
640
|
+
stopReason: "aborted",
|
|
641
|
+
errorMessage: finalMessage.errorMessage,
|
|
642
|
+
errorSubtype: "aborted",
|
|
643
|
+
});
|
|
644
|
+
}
|
|
523
645
|
function finishStandardLoop(stream, newMessages, options) {
|
|
524
646
|
stream.push({
|
|
525
647
|
type: "agent_result",
|
|
@@ -17,6 +17,7 @@ import { computeRecoveryMaxTokens, createOutputTokenRecoveryMessage, createToken
|
|
|
17
17
|
import { createInterruptedToolResults, createSkippedToolCallLimitResults, enforceToolResultBatchSize, } from "./agent-loop-tool-results.js";
|
|
18
18
|
import { flushReadyToolUseSummaries, startToolUseSummary, } from "./agent-loop-tool-summaries.js";
|
|
19
19
|
import { buildAgentRunPolicy, resolveAgentRunLoopFramework } from "./agent-run-result.js";
|
|
20
|
+
import { waitForAbortableOperation, waitForAssistantStream, waitForAssistantStreamEvent, } from "./agent-loop-stream-events.js";
|
|
20
21
|
const DEFAULT_MAX_TURNS_PER_PROMPT = 64;
|
|
21
22
|
const DEFAULT_MAX_TOOL_CALLS_PER_PROMPT = 128;
|
|
22
23
|
const DEFAULT_MAX_OUTPUT_TOKEN_RECOVERY_ATTEMPTS = 1;
|
|
@@ -76,13 +77,14 @@ async function runStructuredAdaptiveQueryLoop(currentContext, newMessages, confi
|
|
|
76
77
|
const maxOutputTokenRecoveryAttempts = config.maxOutputTokenRecoveryAttempts ?? DEFAULT_MAX_OUTPUT_TOKEN_RECOVERY_ATTEMPTS;
|
|
77
78
|
const maxStopHookContinuations = config.maxStopHookContinuations ?? DEFAULT_MAX_STOP_HOOK_CONTINUATIONS;
|
|
78
79
|
const maxModelErrorRecoveryAttempts = config.maxModelErrorRecoveryAttempts ?? DEFAULT_MAX_MODEL_ERROR_RECOVERY_ATTEMPTS;
|
|
80
|
+
const initialSteeringMessages = await waitForAbortableOperation(config.getSteeringMessages ? config.getSteeringMessages() : [], signal);
|
|
79
81
|
const state = {
|
|
80
82
|
config,
|
|
81
83
|
turnCount: 0,
|
|
82
84
|
toolCallCount: 0,
|
|
83
85
|
transition: { reason: "start" },
|
|
84
86
|
transitions: [],
|
|
85
|
-
pendingMessages:
|
|
87
|
+
pendingMessages: initialSteeringMessages.type === "resolved" ? initialSteeringMessages.value || [] : [],
|
|
86
88
|
pendingToolUseSummaries: [],
|
|
87
89
|
stopHookActive: false,
|
|
88
90
|
stopHookContinuationCount: 0,
|
|
@@ -94,6 +96,10 @@ async function runStructuredAdaptiveQueryLoop(currentContext, newMessages, confi
|
|
|
94
96
|
usage: emptyUsage(),
|
|
95
97
|
permissionDenials: [],
|
|
96
98
|
};
|
|
99
|
+
if (initialSteeringMessages.type === "aborted") {
|
|
100
|
+
finishStructuredAdaptiveWithAbortedTurn(stream, currentContext, newMessages, state);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
97
103
|
let firstTurn = true;
|
|
98
104
|
while (true) {
|
|
99
105
|
if (!firstTurn) {
|
|
@@ -208,12 +214,17 @@ async function runStructuredAdaptiveQueryLoop(currentContext, newMessages, confi
|
|
|
208
214
|
}
|
|
209
215
|
if (config.runStopHooks && !state.stopHookActive) {
|
|
210
216
|
state.stopHookActive = true;
|
|
211
|
-
const stopHookResult = await config.runStopHooks({
|
|
217
|
+
const stopHookResult = await waitForAbortableOperation(config.runStopHooks({
|
|
212
218
|
message,
|
|
213
219
|
messages: currentContext.messages,
|
|
214
|
-
});
|
|
220
|
+
}), signal);
|
|
215
221
|
state.stopHookActive = false;
|
|
216
|
-
if (stopHookResult.
|
|
222
|
+
if (stopHookResult.type === "aborted") {
|
|
223
|
+
finishStructuredAdaptiveWithAbortedTurn(stream, currentContext, newMessages, state);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const resolvedStopHookResult = stopHookResult.value;
|
|
227
|
+
if (resolvedStopHookResult.action === "continue" && resolvedStopHookResult.messages.length > 0) {
|
|
217
228
|
if (state.stopHookContinuationCount >= maxStopHookContinuations) {
|
|
218
229
|
const limitMessage = createLoopLimitMessage(config, `stop_hook_limit_reached: stopped after ${maxStopHookContinuations} stop-hook continuation turns.`);
|
|
219
230
|
currentContext.messages.push(limitMessage);
|
|
@@ -233,7 +244,7 @@ async function runStructuredAdaptiveQueryLoop(currentContext, newMessages, confi
|
|
|
233
244
|
return;
|
|
234
245
|
}
|
|
235
246
|
state.stopHookContinuationCount += 1;
|
|
236
|
-
state.pendingMessages =
|
|
247
|
+
state.pendingMessages = resolvedStopHookResult.messages;
|
|
237
248
|
recordTransition(state, {
|
|
238
249
|
reason: "stop_hook_blocking",
|
|
239
250
|
continuationCount: state.stopHookContinuationCount,
|
|
@@ -253,7 +264,12 @@ async function runStructuredAdaptiveQueryLoop(currentContext, newMessages, confi
|
|
|
253
264
|
});
|
|
254
265
|
continue;
|
|
255
266
|
}
|
|
256
|
-
const
|
|
267
|
+
const followUpMessagesResult = await waitForAbortableOperation(config.getFollowUpMessages ? config.getFollowUpMessages() : [], signal);
|
|
268
|
+
if (followUpMessagesResult.type === "aborted") {
|
|
269
|
+
finishStructuredAdaptiveWithAbortedTurn(stream, currentContext, newMessages, state);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const followUpMessages = followUpMessagesResult.value || [];
|
|
257
273
|
if (followUpMessages.length === 0) {
|
|
258
274
|
break;
|
|
259
275
|
}
|
|
@@ -329,16 +345,28 @@ async function runStructuredAdaptiveQueryLoop(currentContext, newMessages, confi
|
|
|
329
345
|
async function streamAssistantResponse(context, config, signal, stream, streamFn, maxTokensOverride, streamingToolExecutor) {
|
|
330
346
|
let messages = context.messages;
|
|
331
347
|
if (config.transformContext) {
|
|
332
|
-
|
|
348
|
+
const transformedMessages = await waitForAbortableOperation(config.transformContext(messages, signal), signal);
|
|
349
|
+
if (transformedMessages.type === "aborted") {
|
|
350
|
+
return pushAbortedAssistantMessage(context, stream, config);
|
|
351
|
+
}
|
|
352
|
+
messages = transformedMessages.value;
|
|
353
|
+
}
|
|
354
|
+
const convertedMessages = await waitForAbortableOperation(config.convertToLlm(messages), signal);
|
|
355
|
+
if (convertedMessages.type === "aborted") {
|
|
356
|
+
return pushAbortedAssistantMessage(context, stream, config);
|
|
333
357
|
}
|
|
334
|
-
const llmMessages =
|
|
358
|
+
const llmMessages = convertedMessages.value;
|
|
335
359
|
const llmContext = {
|
|
336
360
|
systemPrompt: context.systemPrompt,
|
|
337
361
|
messages: llmMessages,
|
|
338
362
|
tools: context.tools,
|
|
339
363
|
};
|
|
340
364
|
const streamFunction = streamFn || streamSimple;
|
|
341
|
-
const
|
|
365
|
+
const apiKeyResult = await waitForAbortableOperation(config.getApiKey ? config.getApiKey(config.model.provider) : undefined, signal);
|
|
366
|
+
if (apiKeyResult.type === "aborted") {
|
|
367
|
+
return pushAbortedAssistantMessage(context, stream, config);
|
|
368
|
+
}
|
|
369
|
+
const resolvedApiKey = apiKeyResult.value || config.apiKey;
|
|
342
370
|
stream.push({
|
|
343
371
|
type: "stream_request_start",
|
|
344
372
|
model: config.model.id,
|
|
@@ -347,15 +375,58 @@ async function streamAssistantResponse(context, config, signal, stream, streamFn
|
|
|
347
375
|
messageCount: llmMessages.length,
|
|
348
376
|
maxTokens: maxTokensOverride ?? config.maxTokens,
|
|
349
377
|
});
|
|
350
|
-
const response = await streamFunction(config.model, llmContext, {
|
|
378
|
+
const response = await waitForAssistantStream(streamFunction(config.model, llmContext, {
|
|
351
379
|
...config,
|
|
352
380
|
maxTokens: maxTokensOverride ?? config.maxTokens,
|
|
353
381
|
apiKey: resolvedApiKey,
|
|
354
382
|
signal,
|
|
355
|
-
});
|
|
383
|
+
}), signal);
|
|
384
|
+
if (response === "aborted") {
|
|
385
|
+
const finalMessage = createLoopLimitMessage(config, "Request was aborted");
|
|
386
|
+
finalMessage.stopReason = "aborted";
|
|
387
|
+
context.messages.push(finalMessage);
|
|
388
|
+
stream.push({ type: "message_start", message: { ...finalMessage } });
|
|
389
|
+
stream.push({ type: "message_end", message: finalMessage });
|
|
390
|
+
return finalMessage;
|
|
391
|
+
}
|
|
356
392
|
let partialMessage = null;
|
|
357
393
|
let addedPartial = false;
|
|
358
|
-
|
|
394
|
+
const responseIterator = response[Symbol.asyncIterator]();
|
|
395
|
+
while (true) {
|
|
396
|
+
let nextEvent;
|
|
397
|
+
try {
|
|
398
|
+
nextEvent = await waitForAssistantStreamEvent(responseIterator, signal);
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
const finalMessage = createLoopLimitMessage(config, error instanceof Error ? error.message : String(error));
|
|
402
|
+
if (addedPartial) {
|
|
403
|
+
context.messages[context.messages.length - 1] = finalMessage;
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
context.messages.push(finalMessage);
|
|
407
|
+
stream.push({ type: "message_start", message: { ...finalMessage } });
|
|
408
|
+
}
|
|
409
|
+
stream.push({ type: "message_end", message: finalMessage });
|
|
410
|
+
return finalMessage;
|
|
411
|
+
}
|
|
412
|
+
if (nextEvent === "aborted") {
|
|
413
|
+
void responseIterator.return?.();
|
|
414
|
+
const finalMessage = createLoopLimitMessage(config, "Request was aborted");
|
|
415
|
+
finalMessage.stopReason = "aborted";
|
|
416
|
+
if (addedPartial) {
|
|
417
|
+
context.messages[context.messages.length - 1] = finalMessage;
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
context.messages.push(finalMessage);
|
|
421
|
+
stream.push({ type: "message_start", message: { ...finalMessage } });
|
|
422
|
+
}
|
|
423
|
+
stream.push({ type: "message_end", message: finalMessage });
|
|
424
|
+
return finalMessage;
|
|
425
|
+
}
|
|
426
|
+
if (nextEvent.done) {
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
const event = nextEvent.value;
|
|
359
430
|
switch (event.type) {
|
|
360
431
|
case "start":
|
|
361
432
|
partialMessage = event.partial;
|
|
@@ -395,7 +466,7 @@ async function streamAssistantResponse(context, config, signal, stream, streamFn
|
|
|
395
466
|
break;
|
|
396
467
|
case "done":
|
|
397
468
|
case "error": {
|
|
398
|
-
const finalMessage =
|
|
469
|
+
const finalMessage = event.type === "done" ? event.message : event.error;
|
|
399
470
|
if (addedPartial) {
|
|
400
471
|
context.messages[context.messages.length - 1] = finalMessage;
|
|
401
472
|
}
|
|
@@ -410,7 +481,8 @@ async function streamAssistantResponse(context, config, signal, stream, streamFn
|
|
|
410
481
|
}
|
|
411
482
|
}
|
|
412
483
|
}
|
|
413
|
-
const finalMessage =
|
|
484
|
+
const finalMessage = response.resultIfResolved() ??
|
|
485
|
+
createLoopLimitMessage(config, "Provider stream ended without a final assistant message");
|
|
414
486
|
if (addedPartial) {
|
|
415
487
|
context.messages[context.messages.length - 1] = finalMessage;
|
|
416
488
|
}
|
|
@@ -421,6 +493,27 @@ async function streamAssistantResponse(context, config, signal, stream, streamFn
|
|
|
421
493
|
stream.push({ type: "message_end", message: finalMessage });
|
|
422
494
|
return finalMessage;
|
|
423
495
|
}
|
|
496
|
+
function pushAbortedAssistantMessage(context, stream, config) {
|
|
497
|
+
const finalMessage = createLoopLimitMessage(config, "Request was aborted");
|
|
498
|
+
finalMessage.stopReason = "aborted";
|
|
499
|
+
context.messages.push(finalMessage);
|
|
500
|
+
stream.push({ type: "message_start", message: { ...finalMessage } });
|
|
501
|
+
stream.push({ type: "message_end", message: finalMessage });
|
|
502
|
+
return finalMessage;
|
|
503
|
+
}
|
|
504
|
+
function finishStructuredAdaptiveWithAbortedTurn(stream, context, newMessages, state) {
|
|
505
|
+
const finalMessage = createLoopLimitMessage(state.config, "Request was aborted");
|
|
506
|
+
finalMessage.stopReason = "aborted";
|
|
507
|
+
context.messages.push(finalMessage);
|
|
508
|
+
newMessages.push(finalMessage);
|
|
509
|
+
stream.push({ type: "message_start", message: { ...finalMessage } });
|
|
510
|
+
stream.push({ type: "message_end", message: finalMessage });
|
|
511
|
+
stream.push({ type: "turn_end", message: finalMessage, toolResults: [] });
|
|
512
|
+
state.finalStopReason = "aborted";
|
|
513
|
+
state.finalErrorMessage = finalMessage.errorMessage;
|
|
514
|
+
state.finalErrorSubtype = "aborted";
|
|
515
|
+
finish(stream, newMessages, state);
|
|
516
|
+
}
|
|
424
517
|
function createLoopLimitMessage(config, errorMessage) {
|
|
425
518
|
return {
|
|
426
519
|
role: "assistant",
|
|
@@ -113,6 +113,54 @@ function createStreamErrorMessage(model, error) {
|
|
|
113
113
|
function createMissingStreamResultMessage(model) {
|
|
114
114
|
return createStreamErrorMessage(model, new Error("Provider stream ended without a final assistant message"));
|
|
115
115
|
}
|
|
116
|
+
function createAbortMessage(model) {
|
|
117
|
+
return createStreamErrorMessage(model, new Error("Request was aborted"));
|
|
118
|
+
}
|
|
119
|
+
function emitAbortError(stream, model) {
|
|
120
|
+
stream.push({ type: "error", reason: "error", error: createAbortMessage(model) });
|
|
121
|
+
}
|
|
122
|
+
function waitForRetryDelay(delayMs, signal) {
|
|
123
|
+
if (signal?.aborted)
|
|
124
|
+
return Promise.resolve("aborted");
|
|
125
|
+
return new Promise((resolve) => {
|
|
126
|
+
let timeout;
|
|
127
|
+
const cleanup = () => {
|
|
128
|
+
if (timeout !== undefined)
|
|
129
|
+
clearTimeout(timeout);
|
|
130
|
+
signal?.removeEventListener("abort", onAbort);
|
|
131
|
+
};
|
|
132
|
+
const onAbort = () => {
|
|
133
|
+
cleanup();
|
|
134
|
+
resolve("aborted");
|
|
135
|
+
};
|
|
136
|
+
timeout = setTimeout(() => {
|
|
137
|
+
cleanup();
|
|
138
|
+
resolve("elapsed");
|
|
139
|
+
}, delayMs);
|
|
140
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function waitForStreamEvent(iterator, signal) {
|
|
144
|
+
if (signal?.aborted)
|
|
145
|
+
return Promise.resolve("aborted");
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
const cleanup = () => {
|
|
148
|
+
signal?.removeEventListener("abort", onAbort);
|
|
149
|
+
};
|
|
150
|
+
const onAbort = () => {
|
|
151
|
+
cleanup();
|
|
152
|
+
resolve("aborted");
|
|
153
|
+
};
|
|
154
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
155
|
+
iterator.next().then((result) => {
|
|
156
|
+
cleanup();
|
|
157
|
+
resolve(result);
|
|
158
|
+
}, (error) => {
|
|
159
|
+
cleanup();
|
|
160
|
+
reject(error);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
116
164
|
// =============================================================================
|
|
117
165
|
// Provider Resolution
|
|
118
166
|
// =============================================================================
|
|
@@ -163,18 +211,7 @@ function wrapWithRetry(model, createStream, retryOptions, signal) {
|
|
|
163
211
|
let attempt = 0;
|
|
164
212
|
while (attempt <= retryOptions.maxRetries) {
|
|
165
213
|
if (signal?.aborted) {
|
|
166
|
-
|
|
167
|
-
role: "assistant",
|
|
168
|
-
content: [],
|
|
169
|
-
api: model.api,
|
|
170
|
-
provider: model.provider,
|
|
171
|
-
model: model.id,
|
|
172
|
-
stopReason: "error",
|
|
173
|
-
errorMessage: "Request was aborted",
|
|
174
|
-
usage: emptyUsage(),
|
|
175
|
-
timestamp: Date.now(),
|
|
176
|
-
};
|
|
177
|
-
outerStream.push({ type: "error", reason: "error", error: errorMessage });
|
|
214
|
+
emitAbortError(outerStream, model);
|
|
178
215
|
return;
|
|
179
216
|
}
|
|
180
217
|
let innerStream;
|
|
@@ -186,7 +223,10 @@ function wrapWithRetry(model, createStream, retryOptions, signal) {
|
|
|
186
223
|
const delayMs = getRetryDelayMs(errorMessage, attempt, retryOptions);
|
|
187
224
|
if (delayMs !== undefined) {
|
|
188
225
|
attempt++;
|
|
189
|
-
|
|
226
|
+
if ((await waitForRetryDelay(delayMs, signal)) === "aborted") {
|
|
227
|
+
emitAbortError(outerStream, model);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
190
230
|
continue;
|
|
191
231
|
}
|
|
192
232
|
outerStream.push({ type: "error", reason: "error", error: errorMessage });
|
|
@@ -194,7 +234,35 @@ function wrapWithRetry(model, createStream, retryOptions, signal) {
|
|
|
194
234
|
}
|
|
195
235
|
// Forward all events from inner to outer, but intercept the final result
|
|
196
236
|
let lastMessage = null;
|
|
197
|
-
|
|
237
|
+
const innerIterator = innerStream[Symbol.asyncIterator]();
|
|
238
|
+
while (true) {
|
|
239
|
+
let nextEvent;
|
|
240
|
+
try {
|
|
241
|
+
nextEvent = await waitForStreamEvent(innerIterator, signal);
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
lastMessage = createStreamErrorMessage(model, error);
|
|
245
|
+
const delayMs = getRetryDelayMs(lastMessage, attempt, retryOptions);
|
|
246
|
+
if (delayMs !== undefined) {
|
|
247
|
+
attempt++;
|
|
248
|
+
if ((await waitForRetryDelay(delayMs, signal)) === "aborted") {
|
|
249
|
+
emitAbortError(outerStream, model);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
outerStream.push({ type: "error", reason: "error", error: lastMessage });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (nextEvent === "aborted") {
|
|
258
|
+
void innerIterator.return?.();
|
|
259
|
+
emitAbortError(outerStream, model);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (nextEvent.done) {
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
const event = nextEvent.value;
|
|
198
266
|
if (event.type === "done") {
|
|
199
267
|
lastMessage = event.message;
|
|
200
268
|
outerStream.push(event);
|
|
@@ -206,7 +274,10 @@ function wrapWithRetry(model, createStream, retryOptions, signal) {
|
|
|
206
274
|
const delayMs = getRetryDelayMs(lastMessage, attempt, retryOptions);
|
|
207
275
|
if (delayMs !== undefined) {
|
|
208
276
|
attempt++;
|
|
209
|
-
|
|
277
|
+
if ((await waitForRetryDelay(delayMs, signal)) === "aborted") {
|
|
278
|
+
emitAbortError(outerStream, model);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
210
281
|
break; // Break inner loop, retry outer loop
|
|
211
282
|
}
|
|
212
283
|
// Non-retriable or max retries exhausted — forward error
|
|
@@ -226,7 +297,10 @@ function wrapWithRetry(model, createStream, retryOptions, signal) {
|
|
|
226
297
|
const delayMs = getRetryDelayMs(lastMessage, attempt, retryOptions);
|
|
227
298
|
if (delayMs !== undefined) {
|
|
228
299
|
attempt++;
|
|
229
|
-
|
|
300
|
+
if ((await waitForRetryDelay(delayMs, signal)) === "aborted") {
|
|
301
|
+
emitAbortError(outerStream, model);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
230
304
|
continue;
|
|
231
305
|
}
|
|
232
306
|
if (lastMessage.stopReason === "error" || lastMessage.stopReason === "aborted") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pencil-agent/nano-pencil",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.6",
|
|
4
4
|
"description": "CLI writing agent with read, bash, edit, write tools and session management. Supports DashScope and Ali Token Plan. Soul enabled by default for AI personality evolution.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|