@code-yeongyu/senpi-agent-core 2026.10.25-3 → 2026.10.26-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-loop.js +39 -13
- package/dist/agent.js +5 -0
- package/dist/internal-events.d.ts +29 -0
- package/dist/internal-events.js +38 -0
- package/package.json +3 -3
package/dist/agent-loop.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Agent loop that works with AgentMessage throughout.
|
|
3
3
|
* Transforms to Message[] only at the LLM call boundary.
|
|
4
4
|
*/
|
|
5
|
-
import { createInitialSystemMessage, EventStream, getCurrentTools, getToolStateChanges, isCursorExecResolved, normalizeContext, supportsAllowedToolChoice, validateToolArguments, } from "@earendil-works/pi-ai";
|
|
5
|
+
import { assistantRecords, carryAssistantCapture, getAssistantCapture, createInitialSystemMessage, EventStream, getCurrentTools, getToolStateChanges, isCursorExecResolved, isAssistantProgress, normalizeContext, supportsAllowedToolChoice, validateToolArguments, } from "@earendil-works/pi-ai";
|
|
6
6
|
import { createTerminalFailureAssistantMessage, isStreamIdleTimeoutError, normalizeTerminalAssistantMessage, settleStopReason, shouldFinalizeIdleAsStop, shouldTerminateAssistantTurn, } from "./assistant-terminal-state.js";
|
|
7
7
|
import { createResponseTimingRecorder } from "./response-timing.js";
|
|
8
|
+
import { internalProgressSink } from "./internal-events.js";
|
|
8
9
|
import { getDefaultStreamFn, withEmptyAssistantRecovery } from "./stream-fn.js";
|
|
9
10
|
import { prepareAgentToolCallArguments } from "./tool-arguments.js";
|
|
10
11
|
import { resolveCallTool, withToolNameCorrection } from "./tool-name-alias.js";
|
|
@@ -510,7 +511,10 @@ async function streamAssistantResponse(context, config, signal, emit, streamFunc
|
|
|
510
511
|
});
|
|
511
512
|
// Record the requested level, whichever stream function answered.
|
|
512
513
|
const result = async () => Object.assign(await response.result(), { thinkingLevel: config.reasoning ?? "off" });
|
|
513
|
-
|
|
514
|
+
// Without an internal listener the loop reads the public stream exactly as before: no records, no carriers.
|
|
515
|
+
const sink = config[internalProgressSink];
|
|
516
|
+
const records = sink ? assistantRecords(response) : response;
|
|
517
|
+
const iterator = records[Symbol.asyncIterator]();
|
|
514
518
|
const eventReader = createAssistantEventReader(iterator, streamIdleTimeoutMs, requestAbortController.signal, (error) => requestAbortController.abort(error), preStream.remainingStartMs(), response, normalizeTimeoutMs(config.streamStartTimeoutMs));
|
|
515
519
|
try {
|
|
516
520
|
while (true) {
|
|
@@ -518,16 +522,20 @@ async function streamAssistantResponse(context, config, signal, emit, streamFunc
|
|
|
518
522
|
if (next.done)
|
|
519
523
|
break;
|
|
520
524
|
const event = next.value;
|
|
525
|
+
if (isAssistantProgress(event)) {
|
|
526
|
+
await sink?.(event);
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
521
529
|
switch (event.type) {
|
|
522
530
|
case "start":
|
|
523
531
|
responseTiming.started();
|
|
524
532
|
partialMessage = event.partial;
|
|
525
533
|
context.messages.push(partialMessage);
|
|
526
534
|
addedPartial = true;
|
|
527
|
-
await emit({
|
|
535
|
+
await emit(carryAssistantCapture(event, {
|
|
528
536
|
type: "message_start",
|
|
529
537
|
message: { ...partialMessage },
|
|
530
|
-
});
|
|
538
|
+
}));
|
|
531
539
|
break;
|
|
532
540
|
case "text_start":
|
|
533
541
|
case "text_delta":
|
|
@@ -541,6 +549,7 @@ async function streamAssistantResponse(context, config, signal, emit, streamFunc
|
|
|
541
549
|
if (partialMessage) {
|
|
542
550
|
partialMessage = event.partial;
|
|
543
551
|
responseTiming.observe(event);
|
|
552
|
+
let timingProgress;
|
|
544
553
|
if (event.type === "thinking_start" ||
|
|
545
554
|
event.type === "thinking_delta" ||
|
|
546
555
|
event.type === "thinking_end") {
|
|
@@ -556,14 +565,23 @@ async function streamAssistantResponse(context, config, signal, emit, streamFunc
|
|
|
556
565
|
block.startedAt = timing.startedAt;
|
|
557
566
|
if (timing.endedAt !== undefined)
|
|
558
567
|
block.endedAt = timing.endedAt;
|
|
568
|
+
// Stamped after provider capture: it amends this event's seed or end block, so it follows it.
|
|
569
|
+
if (getAssistantCapture(event) && event.type !== "thinking_delta")
|
|
570
|
+
timingProgress = Object.freeze({
|
|
571
|
+
type: "message_block_metadata",
|
|
572
|
+
contentIndex: event.contentIndex,
|
|
573
|
+
...timing,
|
|
574
|
+
});
|
|
559
575
|
}
|
|
560
576
|
}
|
|
561
577
|
context.messages[context.messages.length - 1] = partialMessage;
|
|
562
|
-
await emit({
|
|
578
|
+
await emit(carryAssistantCapture(event, {
|
|
563
579
|
type: "message_update",
|
|
564
580
|
assistantMessageEvent: event,
|
|
565
581
|
message: { ...partialMessage },
|
|
566
|
-
});
|
|
582
|
+
}));
|
|
583
|
+
if (timingProgress)
|
|
584
|
+
await sink?.(timingProgress);
|
|
567
585
|
}
|
|
568
586
|
break;
|
|
569
587
|
case "done":
|
|
@@ -577,12 +595,12 @@ async function streamAssistantResponse(context, config, signal, emit, streamFunc
|
|
|
577
595
|
context.messages.push(finalMessage);
|
|
578
596
|
}
|
|
579
597
|
if (!addedPartial) {
|
|
580
|
-
await emit({
|
|
598
|
+
await emit(carryAssistantCapture(event, {
|
|
581
599
|
type: "message_start",
|
|
582
600
|
message: { ...finalMessage },
|
|
583
|
-
});
|
|
601
|
+
}, finalMessage));
|
|
584
602
|
}
|
|
585
|
-
await emit({ type: "message_end", message: finalMessage });
|
|
603
|
+
await emit(carryAssistantCapture(event, { type: "message_end", message: finalMessage }, finalMessage));
|
|
586
604
|
return { message: finalMessage, providerToolResults };
|
|
587
605
|
}
|
|
588
606
|
}
|
|
@@ -749,6 +767,8 @@ configuredStartTimeoutMs) {
|
|
|
749
767
|
const startTimeoutMs = normalizeTimeoutMs(streamStartTimeoutMs);
|
|
750
768
|
const reportedStartTimeoutMs = configuredStartTimeoutMs ?? startTimeoutMs;
|
|
751
769
|
let sawFirstEvent = false;
|
|
770
|
+
// Provider silence since the last public event: internal progress records re-arm neither bound.
|
|
771
|
+
let silentMs = 0;
|
|
752
772
|
let removeAbortListener;
|
|
753
773
|
let abortPromise;
|
|
754
774
|
if (signal !== undefined) {
|
|
@@ -775,10 +795,16 @@ configuredStartTimeoutMs) {
|
|
|
775
795
|
const readTimeoutMs = useStartBound ? startTimeoutMs : idleTimeoutMs;
|
|
776
796
|
const makeTimeoutError = useStartBound
|
|
777
797
|
? (ms) => new StreamStartTimeoutError(reportedStartTimeoutMs ?? ms)
|
|
778
|
-
: (ms) => new StreamIdleTimeoutError(ms);
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
798
|
+
: (ms) => new StreamIdleTimeoutError(idleTimeoutMs ?? ms);
|
|
799
|
+
const readStartedAt = Date.now();
|
|
800
|
+
const result = await readNextAssistantEvent(iterator, readTimeoutMs === undefined ? undefined : Math.max(1, readTimeoutMs - silentMs), makeTimeoutError, abortPromise, onIdleTimeout, stream, signal);
|
|
801
|
+
if (!result.done && isAssistantProgress(result.value))
|
|
802
|
+
silentMs += Date.now() - readStartedAt;
|
|
803
|
+
else {
|
|
804
|
+
silentMs = 0;
|
|
805
|
+
if (!result.done)
|
|
806
|
+
sawFirstEvent = true;
|
|
807
|
+
}
|
|
782
808
|
return result;
|
|
783
809
|
},
|
|
784
810
|
dispose: () => removeAbortListener?.(),
|
package/dist/agent.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readProviderDiagnostic, sanitizeProviderDiagnostic, } from "@earendil-works/pi-ai";
|
|
2
2
|
import { buildProviderContext as buildProviderContextFromAgentContext, runAgentLoop, runAgentLoopContinue, } from "./agent-loop.js";
|
|
3
3
|
import { ProviderRetryWatchdogAbortError } from "./assistant-terminal-state.js";
|
|
4
|
+
import { dispatchInternalAgent, internalLoopSink } from "./internal-events.js";
|
|
4
5
|
import { isLocalFailure, localFailurePort } from "./local-failure.js";
|
|
5
6
|
import { getDefaultStreamFn } from "./stream-fn.js";
|
|
6
7
|
function defaultConvertToLlm(messages) {
|
|
@@ -458,6 +459,7 @@ export class Agent {
|
|
|
458
459
|
const giveBack = (queue, messages) => this.giveBack(queue, messages, queue === "steering" ? steeringQueueGeneration : followUpQueueGeneration);
|
|
459
460
|
const reasoning = this._state.reasoningBaseline ?? this._state.thinkingLevel;
|
|
460
461
|
return {
|
|
462
|
+
...internalLoopSink(this),
|
|
461
463
|
model: this._state.model,
|
|
462
464
|
reasoning: reasoning === "off" ? undefined : reasoning,
|
|
463
465
|
thinkingSelection: this._state.thinkingSelection,
|
|
@@ -653,6 +655,9 @@ export class Agent {
|
|
|
653
655
|
if (!signal) {
|
|
654
656
|
throw new Error("Agent listener invoked outside active run");
|
|
655
657
|
}
|
|
658
|
+
const internal = dispatchInternalAgent(this, { event });
|
|
659
|
+
if (internal)
|
|
660
|
+
await internal;
|
|
656
661
|
for (const listener of this.listeners) {
|
|
657
662
|
await listener(event, signal);
|
|
658
663
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type AssistantProgress } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { AgentEvent, AgentLoopConfig } from "./types.ts";
|
|
3
|
+
export declare const internalProgressSink: unique symbol;
|
|
4
|
+
type ProgressSink = (progress: AssistantProgress) => Promise<void> | undefined;
|
|
5
|
+
export type InternalLoopConfig = AgentLoopConfig & {
|
|
6
|
+
[internalProgressSink]?: ProgressSink;
|
|
7
|
+
};
|
|
8
|
+
type InternalEvent = {
|
|
9
|
+
event: AgentEvent;
|
|
10
|
+
} | {
|
|
11
|
+
progress: AssistantProgress;
|
|
12
|
+
};
|
|
13
|
+
export type OrderedAgentRecord = InternalEvent & {
|
|
14
|
+
readonly sequence: number;
|
|
15
|
+
};
|
|
16
|
+
type Listener = (record: OrderedAgentRecord) => Promise<void> | void;
|
|
17
|
+
/**
|
|
18
|
+
* S02a owner seam: deliberately not exported by the package barrel or subscribed by a host.
|
|
19
|
+
* A listener holds a capture subscription, so provider streams that first push after it build capture records.
|
|
20
|
+
*/
|
|
21
|
+
export declare function subscribeInternalAgent(owner: object, listener: Listener): () => void;
|
|
22
|
+
/** A run started without an internal listener gets no sink, and its loop reads the public stream exactly as before. */
|
|
23
|
+
export declare function internalLoopSink(owner: object): {
|
|
24
|
+
[internalProgressSink]?: ProgressSink;
|
|
25
|
+
};
|
|
26
|
+
/** Returns nothing, so it costs the public pipeline no microtask, while the owner has no internal listener. */
|
|
27
|
+
export declare function dispatchInternalAgent(owner: object, event: InternalEvent): Promise<void> | undefined;
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=internal-events.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { subscribeAssistantCapture } from "@earendil-works/pi-ai";
|
|
2
|
+
export const internalProgressSink = Symbol("internalProgressSink");
|
|
3
|
+
const channels = new WeakMap();
|
|
4
|
+
/**
|
|
5
|
+
* S02a owner seam: deliberately not exported by the package barrel or subscribed by a host.
|
|
6
|
+
* A listener holds a capture subscription, so provider streams that first push after it build capture records.
|
|
7
|
+
*/
|
|
8
|
+
export function subscribeInternalAgent(owner, listener) {
|
|
9
|
+
let channel = channels.get(owner);
|
|
10
|
+
if (!channel) {
|
|
11
|
+
channel = { sequence: 0, listeners: new Set() };
|
|
12
|
+
channels.set(owner, channel);
|
|
13
|
+
}
|
|
14
|
+
channel.listeners.add(listener);
|
|
15
|
+
const release = subscribeAssistantCapture();
|
|
16
|
+
return () => {
|
|
17
|
+
channel.listeners.delete(listener);
|
|
18
|
+
release();
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** A run started without an internal listener gets no sink, and its loop reads the public stream exactly as before. */
|
|
22
|
+
export function internalLoopSink(owner) {
|
|
23
|
+
if (!channels.get(owner)?.listeners.size)
|
|
24
|
+
return {};
|
|
25
|
+
return { [internalProgressSink]: (progress) => dispatchInternalAgent(owner, { progress }) };
|
|
26
|
+
}
|
|
27
|
+
/** Returns nothing, so it costs the public pipeline no microtask, while the owner has no internal listener. */
|
|
28
|
+
export function dispatchInternalAgent(owner, event) {
|
|
29
|
+
const channel = channels.get(owner);
|
|
30
|
+
if (!channel?.listeners.size)
|
|
31
|
+
return undefined;
|
|
32
|
+
const record = Object.freeze({ ...event, sequence: ++channel.sequence });
|
|
33
|
+
return (async () => {
|
|
34
|
+
for (const listener of channel.listeners)
|
|
35
|
+
await listener(record);
|
|
36
|
+
})();
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=internal-events.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-agent-core",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.26-1",
|
|
4
4
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -62,8 +62,8 @@
|
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@earendil-works/chord": "0.99.1",
|
|
65
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.
|
|
66
|
-
"@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.10.
|
|
65
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.26-1",
|
|
66
|
+
"@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.10.26-1",
|
|
67
67
|
"diff": "9.0.0",
|
|
68
68
|
"ignore": "7.0.9",
|
|
69
69
|
"typebox": "1.3.34",
|