@code-yeongyu/senpi-agent-core 2026.9.3 → 2026.9.4-3

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.
@@ -1 +1 @@
1
- {"version":3,"file":"agent-loop.d.ts","sourceRoot":"","sources":["../src/agent-loop.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAIN,KAAK,OAAO,EAEZ,WAAW,EAIX,MAAM,uBAAuB,CAAC;AAU/B,OAAO,KAAK,EACX,YAAY,EACZ,UAAU,EACV,eAAe,EAEf,YAAY,EACZ,SAAS,EACT,aAAa,EAEb,QAAQ,EACR,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAEzE;;;GAGG;AACH,wBAAgB,SAAS,CACxB,OAAO,EAAE,YAAY,EAAE,EACvB,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,QAAQ,EAAE,QAAQ,GAChB,WAAW,CAAC,UAAU,EAAE,YAAY,EAAE,CAAC,CAiBzC;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAChC,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,QAAQ,EAAE,QAAQ,GAChB,WAAW,CAAC,UAAU,EAAE,YAAY,EAAE,CAAC,CAwBzC;AAED,wBAAsB,YAAY,CACjC,OAAO,EAAE,YAAY,EAAE,EACvB,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,QAAQ,EAAE,QAAQ,GAChB,OAAO,CAAC,YAAY,EAAE,CAAC,CAgBzB;AAED,wBAAsB,oBAAoB,CACzC,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,QAAQ,EAAE,QAAQ,GAChB,OAAO,CAAC,YAAY,EAAE,CAAC,CAiBzB;AAmBD,qBAAa,uBAAwB,SAAQ,KAAK;gBACrC,SAAS,EAAE,MAAM;CAM7B;AAqPD,uGAAuG;AACvG,wBAAsB,oBAAoB,CACzC,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,IAAI,CAAC,eAAe,EAAE,cAAc,GAAG,kBAAkB,CAAC,EAClE,MAAM,CAAC,EAAE,WAAW,GAClB,OAAO,CAAC,OAAO,CAAC,CAgBlB;AA+lBD,MAAM,WAAW,qBAAqB;IACrC,QAAQ,EAAE,aAAa,CAAC;IACxB,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;CACd;AAED,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,GAAG,aAAa,CAYrG;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,GAAG,qBAAqB,CAOpG"}
1
+ {"version":3,"file":"agent-loop.d.ts","sourceRoot":"","sources":["../src/agent-loop.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAIN,KAAK,OAAO,EAEZ,WAAW,EAIX,MAAM,uBAAuB,CAAC;AAU/B,OAAO,KAAK,EACX,YAAY,EACZ,UAAU,EACV,eAAe,EAEf,YAAY,EACZ,SAAS,EACT,aAAa,EAEb,QAAQ,EACR,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAEzE;;;GAGG;AACH,wBAAgB,SAAS,CACxB,OAAO,EAAE,YAAY,EAAE,EACvB,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,QAAQ,EAAE,QAAQ,GAChB,WAAW,CAAC,UAAU,EAAE,YAAY,EAAE,CAAC,CAiBzC;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAChC,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,QAAQ,EAAE,QAAQ,GAChB,WAAW,CAAC,UAAU,EAAE,YAAY,EAAE,CAAC,CAwBzC;AAED,wBAAsB,YAAY,CACjC,OAAO,EAAE,YAAY,EAAE,EACvB,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,QAAQ,EAAE,QAAQ,GAChB,OAAO,CAAC,YAAY,EAAE,CAAC,CAgBzB;AAED,wBAAsB,oBAAoB,CACzC,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,QAAQ,EAAE,QAAQ,GAChB,OAAO,CAAC,YAAY,EAAE,CAAC,CAiBzB;AAmBD,qBAAa,uBAAwB,SAAQ,KAAK;gBACrC,SAAS,EAAE,MAAM;CAM7B;AA2ND,uGAAuG;AACvG,wBAAsB,oBAAoB,CACzC,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,IAAI,CAAC,eAAe,EAAE,cAAc,GAAG,kBAAkB,CAAC,EAClE,MAAM,CAAC,EAAE,WAAW,GAClB,OAAO,CAAC,OAAO,CAAC,CAIlB;AA+mBD,MAAM,WAAW,qBAAqB;IACrC,QAAQ,EAAE,aAAa,CAAC;IACxB,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;CACd;AAED,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,GAAG,aAAa,CAYrG;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,GAAG,qBAAqB,CAOpG"}
@@ -96,9 +96,8 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
96
96
  let config = initialConfig;
97
97
  let firstTurn = true;
98
98
  let firstProviderRequest = true;
99
- // Check for steering messages at start (user may have typed while waiting)
100
- let pendingMessages = (await config.getSteeringMessages?.()) || [];
101
99
  let drainedTerminatingQueue;
100
+ let turnStartAlreadyEmitted = false;
102
101
  const refreshTerminatingQueueDrain = async () => {
103
102
  if (!drainedTerminatingQueue || !config.restorePendingMessages)
104
103
  return;
@@ -110,12 +109,17 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
110
109
  drainedTerminatingQueue = pendingMessages.length > 0 ? "followUp" : undefined;
111
110
  }
112
111
  };
112
+ // Check for steering messages at start (user may have typed while waiting)
113
+ let pendingMessages = (await config.getSteeringMessages?.()) || [];
113
114
  // Outer loop: continues when queued follow-up messages arrive after agent would stop
114
115
  while (true) {
115
116
  let hasMoreToolCalls = true;
116
117
  // Inner loop: process tool calls and steering messages
117
118
  while (hasMoreToolCalls || pendingMessages.length > 0) {
118
- if (!firstTurn) {
119
+ if (turnStartAlreadyEmitted) {
120
+ turnStartAlreadyEmitted = false;
121
+ }
122
+ else if (!firstTurn) {
119
123
  await emit({ type: "turn_start" });
120
124
  }
121
125
  else {
@@ -139,10 +143,7 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
139
143
  }
140
144
  pendingMessages = [];
141
145
  }
142
- // Stream assistant response. Continuation-scoped overrides apply to one
143
- // provider request only. Once that request emits its first event, the
144
- // configured idle timeout resumes so healthy reasoning gaps are not bound
145
- // by the short liveness probe.
146
+ // Stream assistant response
146
147
  const isInitialProviderRequest = firstProviderRequest;
147
148
  firstProviderRequest = false;
148
149
  const requestConfig = isInitialProviderRequest
@@ -152,16 +153,10 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
152
153
  streamStartTimeoutMs: config.initialRequestStreamStartTimeoutMs ?? config.streamStartTimeoutMs,
153
154
  }
154
155
  : config;
155
- const streamIdleTimeoutMs = isInitialProviderRequest ? config.timeoutMs : requestConfig.timeoutMs;
156
- const streamed = await streamAssistantResponse(currentContext, requestConfig, signal, emit, withEmptyAssistantRecovery(requestConfig.model, streamFunction), streamIdleTimeoutMs);
156
+ const streamed = await streamAssistantResponse(currentContext, requestConfig, signal, emit, withEmptyAssistantRecovery(requestConfig.model, streamFunction), isInitialProviderRequest ? config.timeoutMs : requestConfig.timeoutMs);
157
157
  const message = promoteStopWithPendingToolCalls(streamed.message);
158
158
  const providerToolResults = streamed.providerToolResults;
159
159
  newMessages.push(message);
160
- // Provider-resolved (Cursor exec-channel) tool results pair with
161
- // already-resolved toolCall blocks in the assistant message. They are
162
- // appended right after it — including on terminal error/abort paths,
163
- // where dropping them would leave resolved calls unpaired and strip
164
- // the interaction from every rebuilt transcript.
165
160
  const toolResults = [];
166
161
  for (const result of providerToolResults) {
167
162
  await emit({ type: "message_start", message: result });
@@ -175,18 +170,14 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
175
170
  await emit({ type: "agent_end", messages: newMessages });
176
171
  return;
177
172
  }
178
- // Check for tool calls. Blocks stamped `kCursorExecResolved` were
179
- // already executed by Cursor's exec channel mid-stream (their results
180
- // arrived via `providerToolResults`); running them here again would
181
- // duplicate side-effecting tools.
173
+ // Check for tool calls
182
174
  const toolCalls = message.content.filter((c) => c.type === "toolCall" && !isCursorExecResolved(c));
183
175
  hasMoreToolCalls = false;
184
176
  let toolBatchTerminated = false;
185
177
  if (toolCalls.length > 0) {
186
- // A native "length" stop means the output was cut off by the token limit,
187
- // so every tool call in the message may carry truncated arguments. Text
188
- // tool-call middleware finalizes its calls as "toolUse", leaving only
189
- // native, unwrapped length responses for this message-wide safeguard.
178
+ // A "length" stop means the output was cut off by the token limit, so
179
+ // every tool call in the message may carry truncated arguments. Fail
180
+ // them all instead of executing potentially borked calls.
190
181
  const executedToolBatch = message.stopReason === "length"
191
182
  ? await failToolCallsFromTruncatedMessage(toolCalls, emit)
192
183
  : await executeToolCalls(currentContext, message, config, signal, emit);
@@ -209,34 +200,35 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
209
200
  context: currentContext,
210
201
  newMessages,
211
202
  };
203
+ if (await config.shouldStopAfterTurn?.(nextTurnContext)) {
204
+ await emit({ type: "agent_end", messages: newMessages });
205
+ return;
206
+ }
212
207
  if (toolBatchTerminated) {
213
- if (await config.shouldStopAfterTurn?.(nextTurnContext)) {
214
- await emit({ type: "agent_end", messages: newMessages });
215
- return;
216
- }
217
208
  pendingMessages = (await config.getSteeringMessages?.()) || [];
218
- if (pendingMessages.length > 0) {
209
+ if (pendingMessages.length > 0)
219
210
  drainedTerminatingQueue = "steering";
220
- }
221
211
  if (pendingMessages.length === 0) {
222
212
  pendingMessages = (await config.getFollowUpMessages?.()) || [];
223
- if (pendingMessages.length > 0) {
213
+ if (pendingMessages.length > 0)
224
214
  drainedTerminatingQueue = "followUp";
225
- }
226
215
  }
227
216
  if (pendingMessages.length === 0) {
228
217
  await emit({ type: "agent_end", messages: newMessages });
229
218
  return;
230
219
  }
220
+ // Give queue owners a boundary before preparation refreshes the drained
221
+ // snapshot, so a clear or replacement wins before admission.
222
+ await emit({ type: "turn_start" });
223
+ turnStartAlreadyEmitted = true;
231
224
  }
232
225
  let nextTurnSnapshot;
233
226
  try {
234
227
  nextTurnSnapshot = await config.prepareNextTurn?.(nextTurnContext);
235
228
  }
236
229
  catch (error) {
237
- if (drainedTerminatingQueue) {
230
+ if (drainedTerminatingQueue)
238
231
  await config.restorePendingMessages?.(drainedTerminatingQueue, pendingMessages);
239
- }
240
232
  throw error;
241
233
  }
242
234
  if (nextTurnSnapshot) {
@@ -256,9 +248,8 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
256
248
  };
257
249
  }
258
250
  if (signal?.aborted) {
259
- if (drainedTerminatingQueue) {
251
+ if (drainedTerminatingQueue)
260
252
  await config.restorePendingMessages?.(drainedTerminatingQueue, pendingMessages);
261
- }
262
253
  await emit({ type: "agent_end", messages: newMessages });
263
254
  return;
264
255
  }
@@ -268,16 +259,7 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
268
259
  await emit({ type: "agent_end", messages: newMessages });
269
260
  return;
270
261
  }
271
- }
272
- if (!toolBatchTerminated &&
273
- (await config.shouldStopAfterTurn?.({
274
- message,
275
- toolResults,
276
- context: currentContext,
277
- newMessages,
278
- }))) {
279
- await emit({ type: "agent_end", messages: newMessages });
280
- return;
262
+ drainedTerminatingQueue = undefined;
281
263
  }
282
264
  if (!toolBatchTerminated) {
283
265
  pendingMessages = (await config.getSteeringMessages?.()) || [];
@@ -297,19 +279,10 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
297
279
  }
298
280
  /** Build the provider context using the same transform and conversion pipeline as an agent request. */
299
281
  export async function buildProviderContext(context, config, signal) {
300
- // Apply context transform if configured (AgentMessage[] → AgentMessage[])
301
282
  let messages = context.messages;
302
- if (config.transformContext) {
283
+ if (config.transformContext)
303
284
  messages = await config.transformContext(messages, signal);
304
- }
305
- // Convert to LLM-compatible messages (AgentMessage[] → Message[])
306
- const llmMessages = await config.convertToLlm(messages);
307
- // Build LLM context
308
- return {
309
- systemPrompt: context.systemPrompt,
310
- messages: llmMessages,
311
- tools: context.tools,
312
- };
285
+ return { systemPrompt: context.systemPrompt, messages: await config.convertToLlm(messages), tools: context.tools };
313
286
  }
314
287
  /**
315
288
  * Stream an assistant response from the LLM.
@@ -695,6 +668,7 @@ async function executeToolCallsSequential(currentContext, assistantMessage, tool
695
668
  }
696
669
  async function executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, signal, emit) {
697
670
  const finalizedCalls = [];
671
+ const preparedCalls = [];
698
672
  let lastSequentialCall;
699
673
  let currentParallelWave = [];
700
674
  for (const toolCall of toolCalls) {
@@ -711,24 +685,44 @@ async function executeToolCallsParallel(currentContext, assistantMessage, toolCa
711
685
  : lastSequentialCall
712
686
  ? [lastSequentialCall]
713
687
  : [];
714
- const finalizedCall = (async () => {
715
- await Promise.all(dependencies);
716
- const finalized = await runPreparedToolCall(currentContext, assistantMessage, preparation, config, signal, emit);
717
- await emitToolExecutionEnd(finalized, emit);
718
- return finalized;
719
- })();
720
- finalizedCalls.push(finalizedCall);
688
+ preparedCalls.push({ preparation, isSequential, dependencies });
721
689
  if (isSequential) {
722
- lastSequentialCall = finalizedCall;
690
+ // Dependencies are assigned in the second phase after all preflight hooks
691
+ // have completed, so a later preflight abort vetoes every execution.
692
+ lastSequentialCall = undefined;
723
693
  currentParallelWave = [];
724
694
  }
725
695
  else {
726
- currentParallelWave.push(finalizedCall);
696
+ currentParallelWave.push(Promise.resolve(undefined));
727
697
  }
728
698
  if (signal?.aborted) {
729
699
  break;
730
700
  }
731
701
  }
702
+ let previousSequential;
703
+ let previousWave = [];
704
+ for (const { preparation, isSequential } of preparedCalls) {
705
+ const dependencies = isSequential
706
+ ? [...(previousSequential ? [previousSequential] : []), ...previousWave]
707
+ : previousSequential
708
+ ? [previousSequential]
709
+ : [];
710
+ const finalizedCall = (async () => {
711
+ await Promise.all(dependencies);
712
+ const finalized = signal?.aborted
713
+ ? { toolCall: preparation.toolCall, result: createErrorToolResult("Operation aborted"), isError: true }
714
+ : await runPreparedToolCall(currentContext, assistantMessage, preparation, config, signal, emit);
715
+ await emitToolExecutionEnd(finalized, emit);
716
+ return finalized;
717
+ })();
718
+ finalizedCalls.push(finalizedCall);
719
+ if (isSequential) {
720
+ previousSequential = finalizedCall;
721
+ previousWave = [];
722
+ }
723
+ else
724
+ previousWave.push(finalizedCall);
725
+ }
732
726
  const orderedFinalizedCalls = await Promise.all(finalizedCalls);
733
727
  const messages = [];
734
728
  for (const finalized of orderedFinalizedCalls) {