@code-yeongyu/senpi-agent-core 2026.10.31-2 → 2026.11.2-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.
@@ -99,7 +99,10 @@ export class StreamStartTimeoutError extends Error {
99
99
  */
100
100
  async function runLoop(initialContext, newMessages, initialConfig, signal, emit, streamFunction) {
101
101
  let currentContext = initialContext;
102
- let config = initialConfig;
102
+ let config = {
103
+ ...initialConfig,
104
+ executionScope: initialConfig.executionScope ?? { id: crypto.randomUUID(), root: "" },
105
+ };
103
106
  let firstTurn = true;
104
107
  let firstProviderRequest = true;
105
108
  let drainedTerminatingQueue;
@@ -922,10 +925,28 @@ async function executeToolCalls(currentContext, assistantMessage, config, signal
922
925
  // channel and its result buffered; running it again would duplicate a
923
926
  // side-effecting tool.
924
927
  const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall" && !isCursorExecResolved(c));
925
- if (config.toolExecution === "sequential") {
926
- return executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit);
927
- }
928
- return executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, signal, emit);
928
+ const host = config.toolHost ?? {
929
+ execute: async (call, options) => {
930
+ // Only yield when a witness sink is configured and returns a promise: an unconditional `await` adds a
931
+ // microtask per call and reorders lifecycle vs message events against today's fused path (EQ E03).
932
+ const recordWitness = options.recordWitness;
933
+ if (recordWitness) {
934
+ for (const raw of call.calls) {
935
+ const pending = recordWitness({ pipeline: "legacy", toolCallId: raw.id });
936
+ if (pending)
937
+ await pending;
938
+ }
939
+ }
940
+ return call.mode === "sequential"
941
+ ? executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, options.signal, options.emit)
942
+ : executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, options.signal, options.emit);
943
+ },
944
+ };
945
+ return host.execute({
946
+ scope: config.executionScope ?? { id: crypto.randomUUID(), root: "" },
947
+ calls: toolCalls,
948
+ mode: config.toolExecution ?? "parallel",
949
+ }, { signal, emit, recordWitness: config.onExecutionPipeline });
929
950
  }
930
951
  async function executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit) {
931
952
  const finalizedCalls = [];
package/dist/agent.d.ts CHANGED
@@ -37,6 +37,9 @@ export interface AgentOptions {
37
37
  streamKeepAliveTimeoutMs?: number;
38
38
  maxRetryDelayMs?: number;
39
39
  toolExecution?: ToolExecutionMode;
40
+ toolHost?: AgentLoopConfig["toolHost"];
41
+ executionScope?: AgentLoopConfig["executionScope"];
42
+ onExecutionPipeline?: AgentLoopConfig["onExecutionPipeline"];
40
43
  removedToolHints?: Record<string, string>;
41
44
  resolveUnknownToolCall?: AgentLoopConfig["resolveUnknownToolCall"];
42
45
  abortServerSideFallback?: boolean;
@@ -113,6 +116,10 @@ export declare class Agent {
113
116
  maxRetryDelayMs?: number;
114
117
  /** Tool execution strategy for assistant messages that contain multiple tool calls. */
115
118
  toolExecution: ToolExecutionMode;
119
+ toolHost?: AgentLoopConfig["toolHost"];
120
+ executionScope: NonNullable<AgentLoopConfig["executionScope"]>;
121
+ getExecutionScope?: () => NonNullable<AgentLoopConfig["executionScope"]>;
122
+ onExecutionPipeline?: AgentLoopConfig["onExecutionPipeline"];
116
123
  /** Migration guidance returned when a removed tool name is called. */
117
124
  removedToolHints: Record<string, string>;
118
125
  /** Optional call-time resolver for tools absent from the request context. */
package/dist/agent.js CHANGED
@@ -167,6 +167,9 @@ export class Agent {
167
167
  this.streamKeepAliveTimeoutMs = runtimeOptions.streamKeepAliveTimeoutMs;
168
168
  this.maxRetryDelayMs = runtimeOptions.maxRetryDelayMs;
169
169
  this.toolExecution = runtimeOptions.toolExecution ?? "parallel";
170
+ this.toolHost = runtimeOptions.toolHost;
171
+ this.executionScope = runtimeOptions.executionScope ?? { id: crypto.randomUUID(), root: "" };
172
+ this.onExecutionPipeline = runtimeOptions.onExecutionPipeline;
170
173
  this.removedToolHints = runtimeOptions.removedToolHints ?? {};
171
174
  this.resolveUnknownToolCall = runtimeOptions.resolveUnknownToolCall;
172
175
  this.abortServerSideFallback = runtimeOptions.abortServerSideFallback;
@@ -485,6 +488,9 @@ export class Agent {
485
488
  abortServerSideFallback: this.abortServerSideFallback,
486
489
  cursorExecHandlers: this.cursorExecHandlers,
487
490
  toolExecution: this.toolExecution,
491
+ toolHost: this.toolHost,
492
+ executionScope: this.getExecutionScope?.() ?? this.executionScope,
493
+ onExecutionPipeline: this.onExecutionPipeline,
488
494
  removedToolHints: this.removedToolHints,
489
495
  resolveUnknownToolCall: this.resolveUnknownToolCall,
490
496
  beforeToolCall: this.beforeToolCall,
@@ -0,0 +1,14 @@
1
+ /**
2
+ * S06: the host-independent loop boundary, relative to packages/agent/src.
3
+ * Every local dependency (including type imports) must also be listed and checked.
4
+ * Host adapters, carriers, persistence implementations and the Node harness stay outside.
5
+ * Extend this closure when adding loop modules; never add a host implementation to it.
6
+ */
7
+ export declare const LOOP_CORE_MODULES: readonly ["agent.ts", "agent-loop.ts", "assistant-terminal-state.ts", "empty-assistant-recovery.ts", "internal-events.ts", "local-failure.ts", "response-timing.ts", "stream-fn.ts", "stream-keepalive.ts", "tool-arguments.ts", "tool-name-alias.ts", "types.ts", "durable/store.ts", "execution/tool-host.ts", "durable/store-admission.ts", "durable/wake-host.ts", "transport/transport.ts"];
8
+ /**
9
+ * Existing portable model/message utilities and schema types, not provider runtimes.
10
+ * Exact specifiers deliberately exclude compat, OAuth, SDKs and host-specific subpaths.
11
+ * The model implementation enters the loop through StreamFn, not a provider import.
12
+ */
13
+ export declare const LOOP_CORE_EXTERNAL_MODULES: readonly ["@earendil-works/pi-ai", "@earendil-works/pi-ai/utils/oauth-refresh-error", "@earendil-works/pi-ai/utils/retry", "@earendil-works/pi-ai/utils/tool-name-match", "typebox"];
14
+ //# sourceMappingURL=loop-core-manifest.d.ts.map
@@ -0,0 +1,38 @@
1
+ /**
2
+ * S06: the host-independent loop boundary, relative to packages/agent/src.
3
+ * Every local dependency (including type imports) must also be listed and checked.
4
+ * Host adapters, carriers, persistence implementations and the Node harness stay outside.
5
+ * Extend this closure when adding loop modules; never add a host implementation to it.
6
+ */
7
+ export const LOOP_CORE_MODULES = [
8
+ "agent.ts",
9
+ "agent-loop.ts",
10
+ "assistant-terminal-state.ts",
11
+ "empty-assistant-recovery.ts",
12
+ "internal-events.ts",
13
+ "local-failure.ts",
14
+ "response-timing.ts",
15
+ "stream-fn.ts",
16
+ "stream-keepalive.ts",
17
+ "tool-arguments.ts",
18
+ "tool-name-alias.ts",
19
+ "types.ts",
20
+ "durable/store.ts",
21
+ "execution/tool-host.ts",
22
+ "durable/store-admission.ts",
23
+ "durable/wake-host.ts",
24
+ "transport/transport.ts",
25
+ ];
26
+ /**
27
+ * Existing portable model/message utilities and schema types, not provider runtimes.
28
+ * Exact specifiers deliberately exclude compat, OAuth, SDKs and host-specific subpaths.
29
+ * The model implementation enters the loop through StreamFn, not a provider import.
30
+ */
31
+ export const LOOP_CORE_EXTERNAL_MODULES = [
32
+ "@earendil-works/pi-ai",
33
+ "@earendil-works/pi-ai/utils/oauth-refresh-error",
34
+ "@earendil-works/pi-ai/utils/retry",
35
+ "@earendil-works/pi-ai/utils/tool-name-match",
36
+ "typebox",
37
+ ];
38
+ //# sourceMappingURL=loop-core-manifest.js.map
@@ -0,0 +1,32 @@
1
+ import type { ToolResultMessage } from "@earendil-works/pi-ai";
2
+ import type { AgentEvent, AgentToolCall, ToolExecutionMode } from "../types.ts";
3
+ /** Brain-minted identity and execution root; the executor treats the identity as opaque. */
4
+ export interface ExecutionScope {
5
+ readonly id: string;
6
+ readonly root: string;
7
+ }
8
+ /** The raw model calls, before name resolution, argument preparation, validation or hooks. */
9
+ export interface ToolHostCall {
10
+ readonly scope: ExecutionScope;
11
+ readonly calls: readonly AgentToolCall[];
12
+ readonly mode: ToolExecutionMode;
13
+ }
14
+ export interface ExecutionPipelineWitness {
15
+ readonly pipeline: "legacy" | "prepared";
16
+ readonly toolCallId: string;
17
+ }
18
+ export interface ToolHostResult {
19
+ readonly messages: ToolResultMessage[];
20
+ readonly terminate: boolean;
21
+ }
22
+ export interface ToolHostOptions {
23
+ readonly signal?: AbortSignal;
24
+ /** Preserve tool events and ordered result-message events. The loop appends the returned messages to context. */
25
+ readonly emit: (event: AgentEvent) => Promise<void> | void;
26
+ /** Receipt metadata only: never an AgentEvent, tool result, provider payload or EQ observable. */
27
+ readonly recordWitness?: (witness: ExecutionPipelineWitness) => Promise<void> | void;
28
+ }
29
+ export interface ToolHost {
30
+ execute(call: ToolHostCall, options: ToolHostOptions): Promise<ToolHostResult>;
31
+ }
32
+ //# sourceMappingURL=tool-host.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=tool-host.js.map
package/dist/types.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import type { Api, AssistantMessage, AssistantMessageEvent, AssistantMessageEventStream, CursorExecHandlers, ImageContent, JsonValue, Message, Model, ProviderDiagnostic, SimpleStreamOptions, TextContent, ThinkingSelection, Tool, ToolResultMessage, TranscriptContext, Usage } from "@earendil-works/pi-ai";
2
2
  import type { Static, TSchema } from "typebox";
3
+ import type { ExecutionPipelineWitness, ExecutionScope, ToolHost } from "./execution/tool-host.ts";
4
+ export type { ExecutionPipelineWitness, ExecutionScope, ToolHost, ToolHostCall, ToolHostOptions, ToolHostResult, } from "./execution/tool-host.ts";
3
5
  /**
4
6
  * Stream function used by the agent loop. `Models.streamSimple` satisfies
5
7
  * this shape.
@@ -322,6 +324,10 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
322
324
  * Default: "parallel"
323
325
  */
324
326
  toolExecution?: ToolExecutionMode;
327
+ /** Unprepared batch boundary. Omitted selects the existing in-process legacy scheduler. */
328
+ toolHost?: ToolHost;
329
+ executionScope?: ExecutionScope;
330
+ onExecutionPipeline?: (witness: ExecutionPipelineWitness) => Promise<void> | void;
325
331
  /**
326
332
  * Optional migration guidance for tool names intentionally removed by an extension.
327
333
  * The matching hint is appended to the normal unknown-tool error before hooks run.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-agent-core",
3
- "version": "2026.10.31-2",
3
+ "version": "2026.11.02-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",
@@ -70,8 +70,8 @@
70
70
  },
71
71
  "dependencies": {
72
72
  "@earendil-works/chord": "0.99.1",
73
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.31-2",
74
- "@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.10.31-2",
73
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.11.02-1",
74
+ "@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.11.02-1",
75
75
  "diff": "9.0.0",
76
76
  "ignore": "7.0.9",
77
77
  "typebox": "1.3.34",