@code-yeongyu/senpi-agent-core 2026.10.26-1 → 2026.10.27-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.d.ts +4 -0
- package/dist/agent.js +5 -3
- package/dist/empty-assistant-recovery.js +26 -19
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/internal-events.d.ts +10 -5
- package/dist/internal-events.js +28 -22
- package/package.json +3 -3
package/dist/agent.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ImageContent, type Message, type SimpleStreamOptions, type ThinkingBudgets, type TranscriptContext, type Transport } from "@earendil-works/pi-ai";
|
|
2
|
+
import { type InternalAgentChannel } from "./internal-events.ts";
|
|
2
3
|
import type { AfterToolCallContext, AfterToolCallResult, AgentContext, AgentEvent, AgentLoopConfig, AgentLoopTurnUpdate, AgentMessage, AgentState, BeforeToolCallContext, BeforeToolCallResult, FinishTurn, PrepareNextTurnContext, PrepareRequest, QueueMode, StreamFn, ToolExecutionMode } from "./types.ts";
|
|
3
4
|
export type { QueueMode } from "./types.ts";
|
|
4
5
|
/**
|
|
@@ -8,6 +9,8 @@ export type { QueueMode } from "./types.ts";
|
|
|
8
9
|
export type AgentInitialState = Partial<Omit<AgentState, "pendingToolCalls" | "isStreaming" | "streamingMessage" | "errorMessage" | "providerDiagnostic">>;
|
|
9
10
|
/** Options for constructing an {@link Agent}. */
|
|
10
11
|
export interface AgentOptions {
|
|
12
|
+
/** @internal Construction-time channel for senpi's session layer only. */
|
|
13
|
+
internalChannel?: InternalAgentChannel;
|
|
11
14
|
initialState?: AgentInitialState;
|
|
12
15
|
convertToLlm?: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
13
16
|
transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
|
|
@@ -74,6 +77,7 @@ export interface AgentContinuationOptions {
|
|
|
74
77
|
* and exposes queueing APIs for steering and follow-up messages.
|
|
75
78
|
*/
|
|
76
79
|
export declare class Agent {
|
|
80
|
+
#private;
|
|
77
81
|
private _state;
|
|
78
82
|
private readonly listeners;
|
|
79
83
|
private readonly steeringQueue;
|
package/dist/agent.js
CHANGED
|
@@ -1,7 +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
|
+
import { dispatchInternalAgent, internalLoopSink, } from "./internal-events.js";
|
|
5
5
|
import { isLocalFailure, localFailurePort } from "./local-failure.js";
|
|
6
6
|
import { getDefaultStreamFn } from "./stream-fn.js";
|
|
7
7
|
function defaultConvertToLlm(messages) {
|
|
@@ -132,6 +132,7 @@ class PendingMessageQueue {
|
|
|
132
132
|
* and exposes queueing APIs for steering and follow-up messages.
|
|
133
133
|
*/
|
|
134
134
|
export class Agent {
|
|
135
|
+
#internalChannel;
|
|
135
136
|
async buildProviderContext(context, signal) {
|
|
136
137
|
return buildProviderContextFromAgentContext(context, { convertToLlm: this.convertToLlm, transformContext: this.transformContext, model: this._state.model }, signal);
|
|
137
138
|
}
|
|
@@ -139,6 +140,7 @@ export class Agent {
|
|
|
139
140
|
this.listeners = new Set();
|
|
140
141
|
// Older compiled consumers may omit options or streamFn even though the current API requires them.
|
|
141
142
|
const runtimeOptions = options ?? {};
|
|
143
|
+
this.#internalChannel = runtimeOptions.internalChannel;
|
|
142
144
|
this._state = createMutableAgentState(runtimeOptions.initialState);
|
|
143
145
|
this.convertToLlm = runtimeOptions.convertToLlm ?? defaultConvertToLlm;
|
|
144
146
|
this.transformContext = runtimeOptions.transformContext;
|
|
@@ -459,7 +461,7 @@ export class Agent {
|
|
|
459
461
|
const giveBack = (queue, messages) => this.giveBack(queue, messages, queue === "steering" ? steeringQueueGeneration : followUpQueueGeneration);
|
|
460
462
|
const reasoning = this._state.reasoningBaseline ?? this._state.thinkingLevel;
|
|
461
463
|
return {
|
|
462
|
-
...internalLoopSink(this),
|
|
464
|
+
...internalLoopSink(this.#internalChannel),
|
|
463
465
|
model: this._state.model,
|
|
464
466
|
reasoning: reasoning === "off" ? undefined : reasoning,
|
|
465
467
|
thinkingSelection: this._state.thinkingSelection,
|
|
@@ -655,7 +657,7 @@ export class Agent {
|
|
|
655
657
|
if (!signal) {
|
|
656
658
|
throw new Error("Agent listener invoked outside active run");
|
|
657
659
|
}
|
|
658
|
-
const internal = dispatchInternalAgent(this, { event });
|
|
660
|
+
const internal = dispatchInternalAgent(this.#internalChannel, { event });
|
|
659
661
|
if (internal)
|
|
660
662
|
await internal;
|
|
661
663
|
for (const listener of this.listeners) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createAssistantMessageEventStream, EMPTY_RESPONSE_ERROR, EMPTY_TOOL_USE_ERROR, FORWARDED_EMPTY_RESPONSE_ERROR, FORWARDED_EMPTY_TOOL_USE_ERROR, getToolCallFormat, hasKimiTextToolCallRecovery, hasVisibleAssistantContent, hasVisibleText, shouldRecoverTextToolCalls, } from "@earendil-works/pi-ai";
|
|
1
|
+
import { assistantEvents, carryAssistantCapture, createAssistantMessageEventStream, EMPTY_RESPONSE_ERROR, EMPTY_TOOL_USE_ERROR, FORWARDED_EMPTY_RESPONSE_ERROR, FORWARDED_EMPTY_TOOL_USE_ERROR, getToolCallFormat, hasKimiTextToolCallRecovery, hasVisibleAssistantContent, hasVisibleText, pushAssistantProgress, shouldRecoverTextToolCalls, } from "@earendil-works/pi-ai";
|
|
2
2
|
function isEmptyStop(message) {
|
|
3
3
|
return message.stopReason === "stop" && !hasVisibleAssistantContent(message);
|
|
4
4
|
}
|
|
@@ -56,24 +56,34 @@ function createForwardedEmptyFailure(message, toolUse) {
|
|
|
56
56
|
}
|
|
57
57
|
function createRetryingStream(firstStream, createStream, policy) {
|
|
58
58
|
const outerStream = createAssistantMessageEventStream();
|
|
59
|
+
const forward = (entry) => {
|
|
60
|
+
if ("event" in entry)
|
|
61
|
+
outerStream.push(entry.event);
|
|
62
|
+
else
|
|
63
|
+
pushAssistantProgress(outerStream, entry.progress);
|
|
64
|
+
};
|
|
65
|
+
const replaced = (source, error) => carryAssistantCapture(source, { type: "error", reason: "error", error }, error);
|
|
59
66
|
void (async () => {
|
|
60
67
|
try {
|
|
61
68
|
let stream = firstStream;
|
|
62
69
|
let retrying = false;
|
|
63
70
|
for (;;) {
|
|
71
|
+
// Controls are held beside the events they follow, so a discarded attempt discards both.
|
|
64
72
|
const buffered = [];
|
|
65
73
|
let forwarding = false;
|
|
66
74
|
let retry = false;
|
|
67
|
-
|
|
75
|
+
const events = assistantEvents(stream, (progress) => {
|
|
76
|
+
if (forwarding)
|
|
77
|
+
pushAssistantProgress(outerStream, progress);
|
|
78
|
+
else
|
|
79
|
+
buffered.push({ progress });
|
|
80
|
+
});
|
|
81
|
+
for await (const event of events) {
|
|
68
82
|
if (event.type === "done") {
|
|
69
83
|
const emptyToolUse = isEmptyToolUse(event.message);
|
|
70
84
|
if (isEmptyStop(event.message) || emptyToolUse) {
|
|
71
85
|
if (forwarding) {
|
|
72
|
-
outerStream.push(
|
|
73
|
-
type: "error",
|
|
74
|
-
reason: "error",
|
|
75
|
-
error: createForwardedEmptyFailure(event.message, emptyToolUse),
|
|
76
|
-
});
|
|
86
|
+
outerStream.push(replaced(event, createForwardedEmptyFailure(event.message, emptyToolUse)));
|
|
77
87
|
outerStream.end();
|
|
78
88
|
return;
|
|
79
89
|
}
|
|
@@ -81,20 +91,17 @@ function createRetryingStream(firstStream, createStream, policy) {
|
|
|
81
91
|
retry = true;
|
|
82
92
|
break;
|
|
83
93
|
}
|
|
84
|
-
|
|
85
|
-
outerStream.push({ type: "error", reason: "error", error });
|
|
94
|
+
outerStream.push(replaced(event, createEmptyResponseFailure(event.message, emptyToolUse)));
|
|
86
95
|
outerStream.end();
|
|
87
96
|
return;
|
|
88
97
|
}
|
|
89
|
-
const
|
|
90
|
-
?
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
: event;
|
|
98
|
+
const message = retrying
|
|
99
|
+
? appendRetryDiagnostic(event.message, recoveryDiagnosticType(isEmptyToolUse(event.message)))
|
|
100
|
+
: event.message;
|
|
101
|
+
const terminal = retrying ? carryAssistantCapture(event, { ...event, message }, message) : event;
|
|
95
102
|
if (!forwarding) {
|
|
96
103
|
for (const pending of buffered)
|
|
97
|
-
|
|
104
|
+
forward(pending);
|
|
98
105
|
}
|
|
99
106
|
outerStream.push(terminal);
|
|
100
107
|
outerStream.end();
|
|
@@ -103,7 +110,7 @@ function createRetryingStream(firstStream, createStream, policy) {
|
|
|
103
110
|
if (event.type === "error") {
|
|
104
111
|
if (!forwarding) {
|
|
105
112
|
for (const pending of buffered)
|
|
106
|
-
|
|
113
|
+
forward(pending);
|
|
107
114
|
}
|
|
108
115
|
outerStream.push(event);
|
|
109
116
|
outerStream.end();
|
|
@@ -113,10 +120,10 @@ function createRetryingStream(firstStream, createStream, policy) {
|
|
|
113
120
|
outerStream.push(event);
|
|
114
121
|
continue;
|
|
115
122
|
}
|
|
116
|
-
buffered.push(event);
|
|
123
|
+
buffered.push({ event });
|
|
117
124
|
if (isMeaningfulContentEvent(event, policy)) {
|
|
118
125
|
for (const pending of buffered)
|
|
119
|
-
|
|
126
|
+
forward(pending);
|
|
120
127
|
forwarding = true;
|
|
121
128
|
}
|
|
122
129
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type { AttributeValue, ExactTelemetryAttributes, InferEventAttributes, In
|
|
|
3
3
|
export { createTypedSpanStarter, defineTelemetrySchema, InMemoryTelemetryContext, NOOP_TELEMETRY_CONTEXT, } from "@earendil-works/pi-telemetry";
|
|
4
4
|
export * from "./agent.ts";
|
|
5
5
|
export * from "./agent-loop.ts";
|
|
6
|
+
export { createInternalAgentChannel, type InternalAgentChannel, type OrderedAgentRecord } from "./internal-events.ts";
|
|
6
7
|
export { EMPTY_TOOL_USE_DEMOTION_DIAGNOSTIC, ProviderRetryWatchdogAbortError } from "./assistant-terminal-state.ts";
|
|
7
8
|
export * from "./harness/agent-harness.ts";
|
|
8
9
|
export { type BranchPreparation, type BranchSummaryDetails, type BranchSummaryResult, type CollectEntriesResult, collectEntriesForBranchSummary, type FileOperations, type GenerateBranchSummaryOptions, generateBranchSummary, prepareBranchEntries, } from "./harness/compaction/branch-summarization.ts";
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { uuidv7 } from "@earendil-works/pi-ai";
|
|
|
2
2
|
export { createTypedSpanStarter, defineTelemetrySchema, InMemoryTelemetryContext, NOOP_TELEMETRY_CONTEXT, } from "@earendil-works/pi-telemetry";
|
|
3
3
|
export * from "./agent.js";
|
|
4
4
|
export * from "./agent-loop.js";
|
|
5
|
+
export { createInternalAgentChannel } from "./internal-events.js";
|
|
5
6
|
export { EMPTY_TOOL_USE_DEMOTION_DIAGNOSTIC, ProviderRetryWatchdogAbortError } from "./assistant-terminal-state.js";
|
|
6
7
|
export * from "./harness/agent-harness.js";
|
|
7
8
|
export { collectEntriesForBranchSummary, generateBranchSummary, prepareBranchEntries, } from "./harness/compaction/branch-summarization.js";
|
|
@@ -10,20 +10,25 @@ type InternalEvent = {
|
|
|
10
10
|
} | {
|
|
11
11
|
progress: AssistantProgress;
|
|
12
12
|
};
|
|
13
|
+
/** @internal For senpi's session layer only. */
|
|
13
14
|
export type OrderedAgentRecord = InternalEvent & {
|
|
14
15
|
readonly sequence: number;
|
|
15
16
|
};
|
|
16
17
|
type Listener = (record: OrderedAgentRecord) => Promise<void> | void;
|
|
18
|
+
/** @internal Construction-time capability for senpi's session layer only. */
|
|
19
|
+
export interface InternalAgentChannel {
|
|
20
|
+
subscribe(listener: Listener): () => void;
|
|
21
|
+
}
|
|
17
22
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
23
|
+
* @internal For senpi's session layer only. Pass at Agent construction; an existing Agent cannot be attached.
|
|
24
|
+
* Capture is held from the first listener until the last release, never by an idle channel.
|
|
20
25
|
*/
|
|
21
|
-
export declare function
|
|
26
|
+
export declare function createInternalAgentChannel(): InternalAgentChannel;
|
|
22
27
|
/** 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(
|
|
28
|
+
export declare function internalLoopSink(channel: InternalAgentChannel | undefined): {
|
|
24
29
|
[internalProgressSink]?: ProgressSink;
|
|
25
30
|
};
|
|
26
31
|
/** Returns nothing, so it costs the public pipeline no microtask, while the owner has no internal listener. */
|
|
27
|
-
export declare function dispatchInternalAgent(
|
|
32
|
+
export declare function dispatchInternalAgent(channel: InternalAgentChannel | undefined, event: InternalEvent): Promise<void> | undefined;
|
|
28
33
|
export {};
|
|
29
34
|
//# sourceMappingURL=internal-events.d.ts.map
|
package/dist/internal-events.js
CHANGED
|
@@ -2,36 +2,42 @@ import { subscribeAssistantCapture } from "@earendil-works/pi-ai";
|
|
|
2
2
|
export const internalProgressSink = Symbol("internalProgressSink");
|
|
3
3
|
const channels = new WeakMap();
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* @internal For senpi's session layer only. Pass at Agent construction; an existing Agent cannot be attached.
|
|
6
|
+
* Capture is held from the first listener until the last release, never by an idle channel.
|
|
7
7
|
*/
|
|
8
|
-
export function
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
8
|
+
export function createInternalAgentChannel() {
|
|
9
|
+
const state = { sequence: 0, listeners: new Set() };
|
|
10
|
+
let releaseCapture;
|
|
11
|
+
const channel = Object.freeze({
|
|
12
|
+
subscribe(listener) {
|
|
13
|
+
state.listeners.add(listener);
|
|
14
|
+
releaseCapture ??= subscribeAssistantCapture();
|
|
15
|
+
return () => {
|
|
16
|
+
state.listeners.delete(listener);
|
|
17
|
+
if (state.listeners.size === 0) {
|
|
18
|
+
releaseCapture?.();
|
|
19
|
+
releaseCapture = undefined;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
channels.set(channel, state);
|
|
25
|
+
return channel;
|
|
20
26
|
}
|
|
21
27
|
/** A run started without an internal listener gets no sink, and its loop reads the public stream exactly as before. */
|
|
22
|
-
export function internalLoopSink(
|
|
23
|
-
if (!channels.get(
|
|
28
|
+
export function internalLoopSink(channel) {
|
|
29
|
+
if (!channel || !channels.get(channel)?.listeners.size)
|
|
24
30
|
return {};
|
|
25
|
-
return { [internalProgressSink]: (progress) => dispatchInternalAgent(
|
|
31
|
+
return { [internalProgressSink]: (progress) => dispatchInternalAgent(channel, { progress }) };
|
|
26
32
|
}
|
|
27
33
|
/** Returns nothing, so it costs the public pipeline no microtask, while the owner has no internal listener. */
|
|
28
|
-
export function dispatchInternalAgent(
|
|
29
|
-
const
|
|
30
|
-
if (!
|
|
34
|
+
export function dispatchInternalAgent(channel, event) {
|
|
35
|
+
const state = channel && channels.get(channel);
|
|
36
|
+
if (!state?.listeners.size)
|
|
31
37
|
return undefined;
|
|
32
|
-
const record = Object.freeze({ ...event, sequence: ++
|
|
38
|
+
const record = Object.freeze({ ...event, sequence: ++state.sequence });
|
|
33
39
|
return (async () => {
|
|
34
|
-
for (const listener of
|
|
40
|
+
for (const listener of state.listeners)
|
|
35
41
|
await listener(record);
|
|
36
42
|
})();
|
|
37
43
|
}
|
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.27-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.27-1",
|
|
66
|
+
"@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.10.27-1",
|
|
67
67
|
"diff": "9.0.0",
|
|
68
68
|
"ignore": "7.0.9",
|
|
69
69
|
"typebox": "1.3.34",
|