@code-yeongyu/senpi-agent-core 2026.10.26-1 → 2026.10.26-2

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 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
- for await (const event of stream) {
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
- const error = createEmptyResponseFailure(event.message, emptyToolUse);
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 terminal = retrying
90
- ? {
91
- ...event,
92
- message: appendRetryDiagnostic(event.message, recoveryDiagnosticType(isEmptyToolUse(event.message))),
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
- outerStream.push(pending);
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
- outerStream.push(pending);
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
- outerStream.push(pending);
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
- * 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.
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 subscribeInternalAgent(owner: object, listener: Listener): () => void;
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(owner: object): {
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(owner: object, event: InternalEvent): Promise<void> | undefined;
32
+ export declare function dispatchInternalAgent(channel: InternalAgentChannel | undefined, event: InternalEvent): Promise<void> | undefined;
28
33
  export {};
29
34
  //# sourceMappingURL=internal-events.d.ts.map
@@ -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
- * 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.
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 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
- };
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(owner) {
23
- if (!channels.get(owner)?.listeners.size)
28
+ export function internalLoopSink(channel) {
29
+ if (!channel || !channels.get(channel)?.listeners.size)
24
30
  return {};
25
- return { [internalProgressSink]: (progress) => dispatchInternalAgent(owner, { progress }) };
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(owner, event) {
29
- const channel = channels.get(owner);
30
- if (!channel?.listeners.size)
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: ++channel.sequence });
38
+ const record = Object.freeze({ ...event, sequence: ++state.sequence });
33
39
  return (async () => {
34
- for (const listener of channel.listeners)
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.26-1",
3
+ "version": "2026.10.26-2",
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.26-1",
66
- "@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.10.26-1",
65
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.26-2",
66
+ "@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.10.26-2",
67
67
  "diff": "9.0.0",
68
68
  "ignore": "7.0.9",
69
69
  "typebox": "1.3.34",