@caupulican/pi-agent-core 0.85.3 → 0.85.4

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,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAqB7D,OAAO,KAAK,EACX,YAAY,EACZ,UAAU,EACV,eAAe,EACf,YAAY,EAEZ,aAAa,EAGb,uBAAuB,EACvB,sBAAsB,EACtB,QAAQ,EACR,kBAAkB,EAClB,MAAM,YAAY,CAAC;AAIpB,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,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GACjB,WAAW,CAAC,UAAU,EAAE,YAAY,EAAE,CAAC,CAEzC;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAChC,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GACjB,WAAW,CAAC,UAAU,EAAE,YAAY,EAAE,CAAC,CAIzC;AAuBD,wBAAsB,YAAY,CACjC,OAAO,EAAE,YAAY,EAAE,EACvB,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,cAAc,EACpB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC,CAgBzB;AAED,wBAAsB,oBAAoB,CACzC,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,cAAc,EACpB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC,CAWzB;AAyCD;;;;GAIG;AACH,wBAAsB,gCAAgC,CAAC,OAAO,EAAE;IAC/D,gBAAgB,CAAC,EAAE,CAClB,OAAO,EAAE,uBAAuB,EAChC,MAAM,CAAC,EAAE,WAAW,KAChB,sBAAsB,GAAG,SAAS,GAAG,OAAO,CAAC,sBAAsB,GAAG,SAAS,CAAC,CAAC;IACtF,KAAK,EAAE,uBAAuB,CAAC,OAAO,CAAC,CAAC;IACxC,OAAO,EAAE,uBAAuB,CAAC,SAAS,CAAC,CAAC;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAmB9B;AA2JD;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAC9C,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CA8CxC;AAi3BD,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,aAAa,GAAG,kBAAkB,GAAG,SAAS,CAO7F"}
1
+ {"version":3,"file":"agent-loop.d.ts","sourceRoot":"","sources":["../src/agent-loop.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAyB7D,OAAO,KAAK,EACX,YAAY,EACZ,UAAU,EACV,eAAe,EACf,YAAY,EAEZ,aAAa,EAGb,uBAAuB,EACvB,sBAAsB,EACtB,QAAQ,EACR,kBAAkB,EAClB,MAAM,YAAY,CAAC;AAIpB,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,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GACjB,WAAW,CAAC,UAAU,EAAE,YAAY,EAAE,CAAC,CAEzC;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAChC,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GACjB,WAAW,CAAC,UAAU,EAAE,YAAY,EAAE,CAAC,CAIzC;AAuBD,wBAAsB,YAAY,CACjC,OAAO,EAAE,YAAY,EAAE,EACvB,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,cAAc,EACpB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC,CAgBzB;AAED,wBAAsB,oBAAoB,CACzC,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,cAAc,EACpB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC,CAWzB;AAyFD;;;;GAIG;AACH,wBAAsB,gCAAgC,CAAC,OAAO,EAAE;IAC/D,gBAAgB,CAAC,EAAE,CAClB,OAAO,EAAE,uBAAuB,EAChC,MAAM,CAAC,EAAE,WAAW,KAChB,sBAAsB,GAAG,SAAS,GAAG,OAAO,CAAC,sBAAsB,GAAG,SAAS,CAAC,CAAC;IACtF,KAAK,EAAE,uBAAuB,CAAC,OAAO,CAAC,CAAC;IACxC,OAAO,EAAE,uBAAuB,CAAC,SAAS,CAAC,CAAC;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAmB9B;AAmLD;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAC9C,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CA8CxC;AA47BD,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,aAAa,GAAG,kBAAkB,GAAG,SAAS,CAO7F"}
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { EventStream } from "@caupulican/pi-ai/event-stream";
6
6
  import { streamSimple } from "@caupulican/pi-ai/stream";
7
- import { formatToolRepairStandingRule } from "@caupulican/pi-ai/tool-repair-registry";
7
+ import { formatToolRepairStandingRule, REPEATED_SUCCESSFUL_TOOL_CALL_FAILURE, } from "@caupulican/pi-ai/tool-repair-registry";
8
8
  import { ToolArgumentValidationError, validateToolArguments, } from "@caupulican/pi-ai/validation";
9
9
  import { assessToolFailure, clearToolFailure, createToolFailureMemoryTracker, createToolFailureResult, normalizeToolSignature, rememberToolFailure, sanitizeToolFailureContext, toolFailureCorrection, } from "./tool-failure-memory.js";
10
10
  import { DEFAULT_MAX_STALL_TURNS } from "./types.js";
@@ -98,6 +98,51 @@ function createAgentStream() {
98
98
  * enough that it's indistinguishable from legitimate varied work, so we don't chase it.
99
99
  */
100
100
  const STALL_WINDOW_PERIODS = 4;
101
+ function textProtocolOperationArguments(toolCall) {
102
+ const args = toolCall.arguments;
103
+ if (!args || typeof args !== "object" || Array.isArray(args))
104
+ return args ?? null;
105
+ const record = args;
106
+ if ((toolCall.name === "write" || toolCall.name === "edit") && typeof record.path === "string") {
107
+ return { path: record.path };
108
+ }
109
+ return args;
110
+ }
111
+ function textProtocolBatchSignature(toolCalls) {
112
+ return normalizeToolSignature(toolCalls.map((toolCall) => [toolCall.name, textProtocolOperationArguments(toolCall)]));
113
+ }
114
+ function toolCallRecord(toolCall) {
115
+ const args = toolCall.arguments;
116
+ return args && typeof args === "object" && !Array.isArray(args) ? args : undefined;
117
+ }
118
+ function toolResultPhase(result) {
119
+ if (!result.details || typeof result.details !== "object" || Array.isArray(result.details))
120
+ return undefined;
121
+ const phase = result.details.phase;
122
+ return typeof phase === "string" ? phase : undefined;
123
+ }
124
+ function repeatsSuccessfulTextProtocolBatch(incomingSignature, toolCalls, previous) {
125
+ if (!previous)
126
+ return false;
127
+ if (incomingSignature === previous.signature)
128
+ return true;
129
+ if (toolCalls.length !== previous.calls.length || toolCalls.length !== previous.messages.length)
130
+ return false;
131
+ return toolCalls.every((toolCall, index) => {
132
+ const previousCall = previous.calls[index];
133
+ const previousResult = previous.messages[index];
134
+ if (!previousCall || !previousResult)
135
+ return false;
136
+ const currentArgs = toolCallRecord(toolCall);
137
+ const previousArgs = toolCallRecord(previousCall);
138
+ if (currentArgs?.path !== previousArgs?.path)
139
+ return false;
140
+ const phase = toolResultPhase(previousResult);
141
+ if (phase === "written")
142
+ return toolCall.name === "write" && previousCall.name === "write";
143
+ return phase === "edited" && toolCall.name === "edit" && previousCall.name === "edit";
144
+ });
145
+ }
101
146
  /**
102
147
  * Apply one request-local preflight without mutating persistent loop configuration.
103
148
  * Shared with isolated tool-free provider calls so every transport boundary has identical
@@ -144,6 +189,7 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
144
189
  const validationFailureTracker = { repeats: 0 };
145
190
  const repairTeachTracker = new Map();
146
191
  let toolFailureMemory = createToolFailureMemoryTracker(currentContext.messages);
192
+ let lastSuccessfulTextProtocolBatch;
147
193
  // Check for steering messages at start (user may have typed while waiting)
148
194
  let pendingMessages = (await config.getSteeringMessages?.()) || [];
149
195
  // Outer loop: continues when queued follow-up messages arrive after agent would stop
@@ -159,6 +205,7 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
159
205
  }
160
206
  // Process pending messages (inject before next assistant response)
161
207
  if (pendingMessages.length > 0) {
208
+ lastSuccessfulTextProtocolBatch = undefined;
162
209
  for (const message of pendingMessages) {
163
210
  await emit({ type: "message_start", message });
164
211
  await emit({ type: "message_end", message });
@@ -180,13 +227,34 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
180
227
  const toolResults = [];
181
228
  hasMoreToolCalls = false;
182
229
  if (toolCalls.length > 0) {
183
- const executedToolBatch = await executeToolCalls(currentContext, message, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, signal, emit);
230
+ const textProtocolBatch = toolCalls.every((toolCall) => toolCall.source === "text-protocol");
231
+ const incomingBatchSignature = textProtocolBatchSignature(toolCalls);
232
+ const previousSuccessfulTextProtocolResults = textProtocolBatch &&
233
+ repeatsSuccessfulTextProtocolBatch(incomingBatchSignature, toolCalls, lastSuccessfulTextProtocolBatch)
234
+ ? lastSuccessfulTextProtocolBatch?.messages
235
+ : undefined;
236
+ const executedToolBatch = await executeToolCalls(currentContext, message, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, previousSuccessfulTextProtocolResults, signal, emit);
184
237
  toolResults.push(...executedToolBatch.messages);
185
238
  hasMoreToolCalls = !executedToolBatch.terminate;
186
239
  for (const result of toolResults) {
187
240
  currentContext.messages.push(result);
188
241
  newMessages.push(result);
189
242
  }
243
+ if (!previousSuccessfulTextProtocolResults) {
244
+ lastSuccessfulTextProtocolBatch =
245
+ textProtocolBatch &&
246
+ toolResults.length === toolCalls.length &&
247
+ toolResults.every((result) => !result.isError)
248
+ ? {
249
+ signature: textProtocolBatchSignature(toolCalls),
250
+ messages: toolResults,
251
+ calls: toolCalls,
252
+ }
253
+ : undefined;
254
+ }
255
+ }
256
+ else {
257
+ lastSuccessfulTextProtocolBatch = undefined;
190
258
  }
191
259
  await emit({ type: "turn_end", message, toolResults });
192
260
  // Runaway-loop backstop (cost guard): detect a model stuck repeating one action.
@@ -358,27 +426,36 @@ async function streamAssistantResponse(context, config, signal, emit, streamFn)
358
426
  /**
359
427
  * Execute tool calls from an assistant message.
360
428
  */
361
- async function executeToolCalls(currentContext, assistantMessage, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, signal, emit) {
429
+ async function executeToolCalls(currentContext, assistantMessage, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, previousSuccessfulTextProtocolResults, signal, emit) {
362
430
  const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall");
363
431
  const hasSequentialToolCall = toolCalls.some((tc) => currentContext.tools?.find((t) => t.name === tc.name)?.executionMode === "sequential");
364
432
  if (config.toolExecution === "sequential" || hasSequentialToolCall) {
365
- return executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, signal, emit);
433
+ return executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, previousSuccessfulTextProtocolResults, signal, emit);
366
434
  }
367
- return executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, signal, emit);
435
+ return executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, previousSuccessfulTextProtocolResults, signal, emit);
368
436
  }
369
- async function executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, signal, emit) {
437
+ async function prepareAndStartToolCall(currentContext, assistantMessage, toolCall, index, config, validationFailureTracker, toolFailureMemory, previousSuccessfulTextProtocolResults, signal, emit) {
438
+ const preparation = previousSuccessfulTextProtocolResults
439
+ ? createRepeatedSuccessfulToolCallOutcome(previousSuccessfulTextProtocolResults[index])
440
+ : await prepareToolCall(currentContext, assistantMessage, toolCall, config, validationFailureTracker, signal);
441
+ await emitToolExecutionStart(toolCall, emit);
442
+ if (preparation.kind === "immediate") {
443
+ emitToolArgumentValidationTelemetry(config, preparation.validationEvent, "not_run", "none");
444
+ return { kind: "finalized", finalized: finalizeRejectedToolCall(toolCall, preparation, toolFailureMemory) };
445
+ }
446
+ return { kind: "prepared", preparation };
447
+ }
448
+ async function executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, previousSuccessfulTextProtocolResults, signal, emit) {
370
449
  const finalizedCalls = [];
371
450
  const messages = [];
372
- for (const toolCall of toolCalls) {
373
- const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, validationFailureTracker, signal);
374
- await emitToolExecutionStart(toolCall, emit);
451
+ for (const [index, toolCall] of toolCalls.entries()) {
452
+ const started = await prepareAndStartToolCall(currentContext, assistantMessage, toolCall, index, config, validationFailureTracker, toolFailureMemory, previousSuccessfulTextProtocolResults, signal, emit);
375
453
  let finalized;
376
- if (preparation.kind === "immediate") {
377
- emitToolArgumentValidationTelemetry(config, preparation.validationEvent, "not_run", "none");
378
- finalized = finalizeRejectedToolCall(toolCall, preparation, toolFailureMemory);
454
+ if (started.kind === "finalized") {
455
+ finalized = started.finalized;
379
456
  }
380
457
  else {
381
- finalized = await executeAndFinalizePreparedToolCall(currentContext, assistantMessage, preparation, config, repairTeachTracker, toolFailureMemory, signal, emit);
458
+ finalized = await executeAndFinalizePreparedToolCall(currentContext, assistantMessage, started.preparation, config, repairTeachTracker, toolFailureMemory, signal, emit);
382
459
  }
383
460
  await emitToolExecutionEnd(finalized, emit);
384
461
  const toolResultMessage = createToolResultMessage(finalized);
@@ -394,23 +471,20 @@ async function executeToolCallsSequential(currentContext, assistantMessage, tool
394
471
  terminate: shouldTerminateToolBatch(finalizedCalls),
395
472
  };
396
473
  }
397
- async function executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, signal, emit) {
474
+ async function executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, validationFailureTracker, repairTeachTracker, toolFailureMemory, previousSuccessfulTextProtocolResults, signal, emit) {
398
475
  const finalizedCalls = [];
399
- for (const toolCall of toolCalls) {
400
- const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, validationFailureTracker, signal);
401
- await emitToolExecutionStart(toolCall, emit);
402
- if (preparation.kind === "immediate") {
403
- emitToolArgumentValidationTelemetry(config, preparation.validationEvent, "not_run", "none");
404
- const finalized = finalizeRejectedToolCall(toolCall, preparation, toolFailureMemory);
405
- await emitToolExecutionEnd(finalized, emit);
406
- finalizedCalls.push(finalized);
476
+ for (const [index, toolCall] of toolCalls.entries()) {
477
+ const started = await prepareAndStartToolCall(currentContext, assistantMessage, toolCall, index, config, validationFailureTracker, toolFailureMemory, previousSuccessfulTextProtocolResults, signal, emit);
478
+ if (started.kind === "finalized") {
479
+ await emitToolExecutionEnd(started.finalized, emit);
480
+ finalizedCalls.push(started.finalized);
407
481
  if (signal?.aborted) {
408
482
  break;
409
483
  }
410
484
  continue;
411
485
  }
412
486
  finalizedCalls.push(async () => {
413
- const finalized = await executeAndFinalizePreparedToolCall(currentContext, assistantMessage, preparation, config, repairTeachTracker, toolFailureMemory, signal, emit);
487
+ const finalized = await executeAndFinalizePreparedToolCall(currentContext, assistantMessage, started.preparation, config, repairTeachTracker, toolFailureMemory, signal, emit);
414
488
  await emitToolExecutionEnd(finalized, emit);
415
489
  return finalized;
416
490
  });
@@ -432,6 +506,27 @@ async function executeToolCallsParallel(currentContext, assistantMessage, toolCa
432
506
  }
433
507
  const DEFAULT_TOOL_VALIDATION_ESCALATION_THRESHOLD = 3;
434
508
  const TOOL_REPAIR_TEACH_EVERY = 5;
509
+ const REPEATED_SUCCESS_RESULT_MAX_CHARS = 2_048;
510
+ function createRepeatedSuccessfulToolCallOutcome(previousResult) {
511
+ const previousText = previousResult?.content
512
+ .filter((block) => block.type === "text")
513
+ .map((block) => block.text)
514
+ .join("\n")
515
+ .slice(0, REPEATED_SUCCESS_RESULT_MAX_CHARS);
516
+ const diagnostic = previousText
517
+ ? `${REPEATED_SUCCESSFUL_TOOL_CALL_FAILURE.diagnostic} Previous successful result: ${previousText}`
518
+ : REPEATED_SUCCESSFUL_TOOL_CALL_FAILURE.diagnostic;
519
+ return {
520
+ kind: "immediate",
521
+ result: createErrorToolResult(diagnostic),
522
+ isError: true,
523
+ phase: "execution",
524
+ failureCode: REPEATED_SUCCESSFUL_TOOL_CALL_FAILURE.failureCode,
525
+ correction: REPEATED_SUCCESSFUL_TOOL_CALL_FAILURE.guidance,
526
+ diagnostic,
527
+ ...(previousResult ? { repeatedSuccessfulCall: { previousToolCallId: previousResult.toolCallId } } : {}),
528
+ };
529
+ }
435
530
  function shouldTerminateToolBatch(finalizedCalls) {
436
531
  return finalizedCalls.length > 0 && finalizedCalls.every((finalized) => finalized.result.terminate === true);
437
532
  }
@@ -642,9 +737,18 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
642
737
  }
643
738
  function finalizeRejectedToolCall(toolCall, outcome, tracker) {
644
739
  const record = rememberToolFailure(tracker, toolCall.name, toolCall.arguments, "rejected", outcome.failureCode, outcome.correction, outcome.diagnostic, outcome.phase);
740
+ const failureResult = createToolFailureResult(record, outcome.result.terminate);
645
741
  return {
646
742
  toolCall,
647
- result: createToolFailureResult(record, outcome.result.terminate),
743
+ result: outcome.repeatedSuccessfulCall
744
+ ? {
745
+ ...failureResult,
746
+ details: {
747
+ ...failureResult.details,
748
+ piRepeatedSuccessfulCall: outcome.repeatedSuccessfulCall,
749
+ },
750
+ }
751
+ : failureResult,
648
752
  isError: true,
649
753
  };
650
754
  }