@pencil-agent/nano-pencil 1.14.2 → 1.14.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.
Files changed (104) hide show
  1. package/dist/build-meta.json +3 -3
  2. package/dist/builtin-extensions.d.ts +4 -0
  3. package/dist/builtin-extensions.js +16 -16
  4. package/dist/core/config/settings-manager.d.ts +8 -1
  5. package/dist/core/config/settings-manager.js +9 -0
  6. package/dist/core/extensions/runner.d.ts +70 -4
  7. package/dist/core/extensions/runner.js +188 -8
  8. package/dist/core/extensions/types.d.ts +8 -1
  9. package/dist/core/i18n/slash-commands.d.ts +12 -0
  10. package/dist/core/i18n/slash-commands.js +16 -4
  11. package/dist/core/i18n/slash-commands.zh.d.ts +12 -0
  12. package/dist/core/i18n/slash-commands.zh.js +16 -4
  13. package/dist/core/runtime/agent-session.d.ts +5 -0
  14. package/dist/core/runtime/agent-session.js +85 -27
  15. package/dist/core/runtime/extension-core-bindings.d.ts +3 -3
  16. package/dist/core/runtime/extension-core-bindings.js +73 -20
  17. package/dist/core/runtime/retry-coordinator.d.ts +10 -1
  18. package/dist/core/runtime/retry-coordinator.js +20 -4
  19. package/dist/core/runtime/sdk.js +2 -1
  20. package/dist/core/runtime/slash-command-catalog.d.ts +2 -1
  21. package/dist/core/runtime/slash-command-catalog.js +9 -1
  22. package/dist/core/slash-commands.d.ts +12 -1
  23. package/dist/core/slash-commands.js +81 -32
  24. package/dist/core/telemetry/batching-dispatcher.d.ts +41 -0
  25. package/dist/core/telemetry/batching-dispatcher.js +89 -0
  26. package/dist/core/telemetry/build-meta.d.ts +12 -0
  27. package/dist/core/telemetry/build-meta.js +57 -0
  28. package/dist/core/telemetry/caller-context.d.ts +32 -0
  29. package/dist/core/telemetry/caller-context.js +19 -0
  30. package/dist/core/telemetry/credentials.d.ts +27 -0
  31. package/dist/core/telemetry/credentials.js +87 -0
  32. package/dist/core/telemetry/ext-events.d.ts +89 -0
  33. package/dist/core/telemetry/ext-events.js +189 -0
  34. package/dist/core/telemetry/index.d.ts +13 -0
  35. package/dist/core/telemetry/index.js +6 -0
  36. package/dist/core/telemetry/insforge-base.d.ts +37 -0
  37. package/dist/core/telemetry/insforge-base.js +160 -0
  38. package/dist/core/telemetry/types.d.ts +33 -0
  39. package/dist/core/telemetry/types.js +7 -0
  40. package/dist/extensions/defaults/browser/index.js +14 -6
  41. package/dist/extensions/defaults/btw/index.js +2 -2
  42. package/dist/extensions/defaults/debug/index.js +38 -3
  43. package/dist/extensions/defaults/diagnostics/index.js +12 -0
  44. package/dist/extensions/defaults/grub/grub-parser.d.ts +15 -1
  45. package/dist/extensions/defaults/grub/grub-parser.js +31 -1
  46. package/dist/extensions/defaults/grub/index.d.ts +1 -1
  47. package/dist/extensions/defaults/grub/index.js +4 -3
  48. package/dist/extensions/defaults/interview/index.js +2 -2
  49. package/dist/extensions/defaults/link-world/index.js +14 -6
  50. package/dist/extensions/defaults/loop/cron/cron-scheduler.js +19 -0
  51. package/dist/extensions/defaults/loop/index.js +35 -0
  52. package/dist/extensions/defaults/mcp/index.js +18 -0
  53. package/dist/extensions/defaults/plan/index.js +29 -11
  54. package/dist/extensions/defaults/presence/index.d.ts +12 -2
  55. package/dist/extensions/defaults/presence/index.js +77 -23
  56. package/dist/extensions/defaults/presence/presence-memory.d.ts +2 -1
  57. package/dist/extensions/defaults/presence/presence-memory.js +37 -1
  58. package/dist/extensions/defaults/recap/index.js +12 -0
  59. package/dist/extensions/defaults/sal/eval/insforge-sink.d.ts +6 -17
  60. package/dist/extensions/defaults/sal/eval/insforge-sink.js +40 -183
  61. package/dist/extensions/defaults/sal/index.js +31 -8
  62. package/dist/extensions/defaults/sal/sal-config.d.ts +5 -0
  63. package/dist/extensions/defaults/sal/sal-config.js +15 -82
  64. package/dist/extensions/defaults/security-audit/index.js +141 -83
  65. package/dist/extensions/defaults/subagent/index.js +29 -5
  66. package/dist/extensions/defaults/team/index.js +10 -9
  67. package/dist/extensions/defaults/team/team-parser.d.ts +18 -0
  68. package/dist/extensions/defaults/team/team-parser.js +91 -3
  69. package/dist/extensions/defaults/team/team-ui.js +4 -14
  70. package/dist/extensions/defaults/token-save/index.js +14 -1
  71. package/dist/extensions/optional/export-html/index.js +19 -5
  72. package/dist/extensions/optional/simplify/index.js +11 -5
  73. package/dist/modes/interactive/interactive-mode.d.ts +2 -1
  74. package/dist/modes/interactive/interactive-mode.js +68 -19
  75. package/dist/modes/interactive/slash-command-arguments.d.ts +16 -0
  76. package/dist/modes/interactive/slash-command-arguments.js +97 -0
  77. package/dist/modes/rpc/rpc-mode.d.ts +3 -0
  78. package/dist/modes/rpc/rpc-mode.js +40 -31
  79. package/dist/modes/rpc/rpc-types.d.ts +3 -0
  80. package/dist/node_modules/@pencil-agent/agent-core/agent.d.ts +15 -1
  81. package/dist/node_modules/@pencil-agent/agent-core/agent.js +13 -1
  82. package/dist/node_modules/@pencil-agent/agent-core/index.d.ts +2 -1
  83. package/dist/node_modules/@pencil-agent/agent-core/index.js +2 -1
  84. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-agent-loop.d.ts +1 -1
  85. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-agent-loop.js +293 -20
  86. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-streaming-tool-executor.d.ts +33 -0
  87. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-streaming-tool-executor.js +189 -0
  88. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-tool-orchestration.d.ts +9 -0
  89. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-tool-orchestration.js +30 -5
  90. package/dist/node_modules/@pencil-agent/agent-core/types.d.ts +90 -3
  91. package/dist/node_modules/@pencil-agent/agent-core/types.js +1 -1
  92. package/dist/node_modules/@pencil-agent/ai/models.generated.d.ts +0 -17
  93. package/dist/node_modules/@pencil-agent/ai/models.generated.js +29 -46
  94. package/dist/node_modules/@pencil-agent/tui/autocomplete.d.ts +8 -1
  95. package/dist/node_modules/@pencil-agent/tui/autocomplete.js +18 -2
  96. package/dist/node_modules/@pencil-agent/tui/index.d.ts +1 -1
  97. package/dist/node_modules/@pencil-agent/tui/tui.d.ts +8 -3
  98. package/dist/node_modules/@pencil-agent/tui/tui.js +68 -33
  99. package/dist/packages/mem-core/extension.js +154 -83
  100. package/dist/packages/mem-core/full-insights-sections.d.ts +48 -0
  101. package/dist/packages/mem-core/full-insights-sections.js +231 -0
  102. package/dist/packages/mem-core/full-insights.d.ts +1 -1
  103. package/dist/packages/mem-core/full-insights.js +102 -42
  104. package/package.json +2 -2
@@ -5,17 +5,21 @@
5
5
  */
6
6
  /**
7
7
  * [WHO]: structuredAdaptiveAgentLoop, structuredAdaptiveAgentLoopContinue
8
- * [FROM]: Depends on @pencil-agent/ai, ./types, ./errors, ./structured-adaptive-tool-orchestration
8
+ * [FROM]: Depends on @pencil-agent/ai, ./types, ./errors, ./structured-adaptive-tool-orchestration, ./structured-adaptive-streaming-tool-executor
9
9
  * [TO]: Consumed by packages/agent-core/src/agent.ts and index.ts
10
10
  * [HERE]: packages/agent-core/src/structured-adaptive-agent-loop.ts - selectable structured-adaptive query loop framework
11
11
  */
12
12
  import { EventStream, isContextOverflow, streamSimple, } from "@pencil-agent/ai";
13
- import { runStructuredAdaptiveTools } from "./structured-adaptive-tool-orchestration.js";
13
+ import { resolveMaxToolConcurrency, runStructuredAdaptiveTools, } from "./structured-adaptive-tool-orchestration.js";
14
+ import { StructuredAdaptiveStreamingToolExecutor } from "./structured-adaptive-streaming-tool-executor.js";
14
15
  import { ValidationError } from "./errors.js";
15
16
  const DEFAULT_MAX_TURNS_PER_PROMPT = 64;
16
17
  const DEFAULT_MAX_TOOL_CALLS_PER_PROMPT = 128;
17
18
  const DEFAULT_MAX_OUTPUT_TOKEN_RECOVERY_ATTEMPTS = 1;
18
19
  const DEFAULT_MAX_STOP_HOOK_CONTINUATIONS = 3;
20
+ const DEFAULT_MAX_MODEL_ERROR_RECOVERY_ATTEMPTS = 1;
21
+ const DEFAULT_OUTPUT_TOKEN_BUDGET_THRESHOLD_PCT = 0.9;
22
+ const DEFAULT_OUTPUT_TOKEN_BUDGET_CONTINUATIONS = 3;
19
23
  export function structuredAdaptiveAgentLoop(prompts, context, config, signal, streamFn) {
20
24
  const stream = createAgentStream();
21
25
  (async () => {
@@ -69,14 +73,18 @@ async function runStructuredAdaptiveQueryLoop(currentContext, newMessages, confi
69
73
  const maxToolCalls = config.maxToolCallsPerPrompt ?? DEFAULT_MAX_TOOL_CALLS_PER_PROMPT;
70
74
  const maxOutputTokenRecoveryAttempts = config.maxOutputTokenRecoveryAttempts ?? DEFAULT_MAX_OUTPUT_TOKEN_RECOVERY_ATTEMPTS;
71
75
  const maxStopHookContinuations = config.maxStopHookContinuations ?? DEFAULT_MAX_STOP_HOOK_CONTINUATIONS;
76
+ const maxModelErrorRecoveryAttempts = config.maxModelErrorRecoveryAttempts ?? DEFAULT_MAX_MODEL_ERROR_RECOVERY_ATTEMPTS;
72
77
  const state = {
73
78
  turnCount: 0,
74
79
  toolCallCount: 0,
75
- transition: "start",
80
+ transition: { reason: "start" },
76
81
  pendingMessages: (await config.getSteeringMessages?.()) || [],
82
+ pendingToolUseSummaries: [],
77
83
  stopHookActive: false,
78
84
  stopHookContinuationCount: 0,
79
85
  maxOutputTokensRecoveryCount: 0,
86
+ modelErrorRecoveryCount: 0,
87
+ tokenBudgetContinuationCount: 0,
80
88
  hasAttemptedReactiveCompact: false,
81
89
  startedAt: Date.now(),
82
90
  usage: emptyUsage(),
@@ -90,6 +98,7 @@ async function runStructuredAdaptiveQueryLoop(currentContext, newMessages, confi
90
98
  else {
91
99
  firstTurn = false;
92
100
  }
101
+ flushReadyToolUseSummaries(state, currentContext, newMessages, stream);
93
102
  if (state.pendingMessages.length > 0) {
94
103
  for (const message of state.pendingMessages) {
95
104
  stream.push({ type: "message_start", message });
@@ -113,20 +122,61 @@ async function runStructuredAdaptiveQueryLoop(currentContext, newMessages, confi
113
122
  finish(stream, newMessages, state);
114
123
  return;
115
124
  }
116
- const message = await streamAssistantResponse(currentContext, config, signal, stream, streamFn, state.maxOutputTokensOverride);
125
+ const streamingToolExecutor = new StructuredAdaptiveStreamingToolExecutor(currentContext.tools, signal, stream, resolveMaxToolConcurrency(config.maxToolConcurrency), config.canUseTool);
126
+ const message = await streamAssistantResponse(currentContext, config, signal, stream, streamFn, state.maxOutputTokensOverride, streamingToolExecutor);
117
127
  newMessages.push(message);
118
128
  addUsage(state.usage, message.usage);
119
129
  state.maxOutputTokensOverride = undefined;
120
130
  if (message.stopReason === "error" || message.stopReason === "aborted") {
121
- stream.push({ type: "turn_end", message, toolResults: [] });
131
+ const errorSubtype = message.stopReason === "aborted"
132
+ ? "aborted"
133
+ : isContextOverflow(message, config.model.contextWindow)
134
+ ? "context_overflow"
135
+ : "model_error";
136
+ const failedToolExecution = streamingToolExecutor.size > 0
137
+ ? await streamingToolExecutor.discardAndDrain(message.stopReason === "aborted" ? "assistant stream aborted" : "assistant stream error", message.stopReason === "aborted" ? "cancel" : "all")
138
+ : { toolResults: [], contextMessages: [], permissionDenials: [] };
139
+ const toolResults = enforceToolResultBatchSize(failedToolExecution.toolResults, config.maxToolResultBatchSizeChars);
140
+ state.permissionDenials.push(...failedToolExecution.permissionDenials);
141
+ for (const result of toolResults) {
142
+ currentContext.messages.push(result);
143
+ newMessages.push(result);
144
+ stream.push({ type: "message_start", message: result });
145
+ stream.push({ type: "message_end", message: result });
146
+ }
147
+ for (const contextMessage of failedToolExecution.contextMessages) {
148
+ currentContext.messages.push(contextMessage);
149
+ newMessages.push(contextMessage);
150
+ stream.push({ type: "message_start", message: contextMessage });
151
+ stream.push({ type: "message_end", message: contextMessage });
152
+ }
153
+ if (message.stopReason === "error" &&
154
+ config.recoverModelError &&
155
+ state.modelErrorRecoveryCount < maxModelErrorRecoveryAttempts) {
156
+ const attempt = state.modelErrorRecoveryCount + 1;
157
+ const recovery = await config.recoverModelError({
158
+ message,
159
+ messages: currentContext.messages,
160
+ errorSubtype,
161
+ attempt,
162
+ });
163
+ if (recovery.action === "retry") {
164
+ stream.push({ type: "turn_end", message, toolResults });
165
+ state.modelErrorRecoveryCount = attempt;
166
+ currentContext.messages = recovery.messages;
167
+ state.transition =
168
+ recovery.transition ?? {
169
+ reason: "model_error_recovery",
170
+ subtype: errorSubtype,
171
+ attempt,
172
+ };
173
+ continue;
174
+ }
175
+ }
176
+ stream.push({ type: "turn_end", message, toolResults });
122
177
  state.finalStopReason = message.stopReason;
123
178
  state.finalErrorMessage = message.errorMessage;
124
- state.finalErrorSubtype =
125
- message.stopReason === "aborted"
126
- ? "aborted"
127
- : isContextOverflow(message, config.model.contextWindow)
128
- ? "context_overflow"
129
- : "model_error";
179
+ state.finalErrorSubtype = errorSubtype;
130
180
  finish(stream, newMessages, state);
131
181
  return;
132
182
  }
@@ -138,7 +188,10 @@ async function runStructuredAdaptiveQueryLoop(currentContext, newMessages, confi
138
188
  state.maxOutputTokensRecoveryCount += 1;
139
189
  state.maxOutputTokensOverride = computeRecoveryMaxTokens(config, message);
140
190
  state.pendingMessages = [createOutputTokenRecoveryMessage(state.maxOutputTokensRecoveryCount)];
141
- state.transition = "follow_up";
191
+ state.transition = {
192
+ reason: "max_output_tokens_recovery",
193
+ attempt: state.maxOutputTokensRecoveryCount,
194
+ };
142
195
  continue;
143
196
  }
144
197
  if (config.runStopHooks && !state.stopHookActive) {
@@ -164,16 +217,31 @@ async function runStructuredAdaptiveQueryLoop(currentContext, newMessages, confi
164
217
  }
165
218
  state.stopHookContinuationCount += 1;
166
219
  state.pendingMessages = stopHookResult.messages;
167
- state.transition = "follow_up";
220
+ state.transition = {
221
+ reason: "stop_hook_blocking",
222
+ continuationCount: state.stopHookContinuationCount,
223
+ };
168
224
  continue;
169
225
  }
170
226
  }
227
+ const tokenBudgetContinuation = maybeCreateTokenBudgetContinuation(config, state);
228
+ if (tokenBudgetContinuation) {
229
+ state.tokenBudgetContinuationCount += 1;
230
+ state.pendingMessages = [tokenBudgetContinuation.message];
231
+ state.transition = {
232
+ reason: "token_budget_continuation",
233
+ continuationCount: state.tokenBudgetContinuationCount,
234
+ outputTokens: tokenBudgetContinuation.outputTokens,
235
+ targetTokens: tokenBudgetContinuation.targetTokens,
236
+ };
237
+ continue;
238
+ }
171
239
  const followUpMessages = (await config.getFollowUpMessages?.()) || [];
172
240
  if (followUpMessages.length === 0) {
173
241
  break;
174
242
  }
175
243
  state.pendingMessages = followUpMessages;
176
- state.transition = "follow_up";
244
+ state.transition = { reason: "follow_up" };
177
245
  continue;
178
246
  }
179
247
  if (state.toolCallCount + toolCalls.length > maxToolCalls) {
@@ -190,9 +258,10 @@ async function runStructuredAdaptiveQueryLoop(currentContext, newMessages, confi
190
258
  return;
191
259
  }
192
260
  state.toolCallCount += toolCalls.length;
193
- state.pendingToolUseSummary = toolCalls.map((call) => call.name).join(", ");
194
- const toolExecution = await runStructuredAdaptiveTools(toolCalls, currentContext.tools, signal, stream, config.getSteeringMessages, config.maxToolConcurrency, config.canUseTool);
195
- const toolResults = toolExecution.toolResults;
261
+ const toolExecution = streamingToolExecutor.size > 0
262
+ ? await streamingToolExecutor.drain()
263
+ : await runStructuredAdaptiveTools(toolCalls, currentContext.tools, signal, stream, config.getSteeringMessages, config.maxToolConcurrency, config.canUseTool);
264
+ const toolResults = enforceToolResultBatchSize(toolExecution.toolResults, config.maxToolResultBatchSizeChars);
196
265
  state.permissionDenials.push(...toolExecution.permissionDenials);
197
266
  for (const result of toolResults) {
198
267
  currentContext.messages.push(result);
@@ -206,6 +275,15 @@ async function runStructuredAdaptiveQueryLoop(currentContext, newMessages, confi
206
275
  stream.push({ type: "message_start", message: contextMessage });
207
276
  stream.push({ type: "message_end", message: contextMessage });
208
277
  }
278
+ const pendingSummary = startToolUseSummary(config, {
279
+ assistantMessage: message,
280
+ toolResults,
281
+ contextMessages: toolExecution.contextMessages,
282
+ messages: currentContext.messages,
283
+ });
284
+ if (pendingSummary) {
285
+ state.pendingToolUseSummaries.push(pendingSummary);
286
+ }
209
287
  stream.push({ type: "turn_end", message, toolResults });
210
288
  if (toolExecution.steeringMessages && toolExecution.steeringMessages.length > 0) {
211
289
  state.pendingMessages = toolExecution.steeringMessages;
@@ -213,12 +291,160 @@ async function runStructuredAdaptiveQueryLoop(currentContext, newMessages, confi
213
291
  else {
214
292
  state.pendingMessages = (await config.getSteeringMessages?.()) || [];
215
293
  }
216
- state.transition = "tool_result";
294
+ state.transition = { reason: "tool_result", toolCallCount: toolCalls.length };
217
295
  }
218
296
  state.finalStopReason = state.finalStopReason ?? inferStopReason(newMessages);
219
297
  finish(stream, newMessages, state);
220
298
  }
221
- async function streamAssistantResponse(context, config, signal, stream, streamFn, maxTokensOverride) {
299
+ function flushReadyToolUseSummaries(state, currentContext, newMessages, stream) {
300
+ if (state.pendingToolUseSummaries.length === 0) {
301
+ return;
302
+ }
303
+ const pending = [];
304
+ for (const summary of state.pendingToolUseSummaries) {
305
+ const result = summary.read();
306
+ if (!result.settled) {
307
+ pending.push(summary);
308
+ continue;
309
+ }
310
+ if (!result.value) {
311
+ continue;
312
+ }
313
+ currentContext.messages.push(result.value);
314
+ newMessages.push(result.value);
315
+ stream.push({ type: "message_start", message: result.value });
316
+ stream.push({ type: "message_end", message: result.value });
317
+ }
318
+ state.pendingToolUseSummaries = pending;
319
+ }
320
+ function startToolUseSummary(config, event) {
321
+ if (!config.createToolUseSummary) {
322
+ return undefined;
323
+ }
324
+ try {
325
+ const value = config.createToolUseSummary({
326
+ ...event,
327
+ messages: [...event.messages],
328
+ });
329
+ return trackToolUseSummary(value);
330
+ }
331
+ catch {
332
+ return undefined;
333
+ }
334
+ }
335
+ function trackToolUseSummary(value) {
336
+ if (!value) {
337
+ return undefined;
338
+ }
339
+ if (typeof value.then !== "function") {
340
+ return {
341
+ read: () => ({ settled: true, value: value }),
342
+ };
343
+ }
344
+ let settled = false;
345
+ let settledValue;
346
+ value.then((summary) => {
347
+ settled = true;
348
+ settledValue = summary;
349
+ }, () => {
350
+ settled = true;
351
+ settledValue = undefined;
352
+ });
353
+ return {
354
+ read: () => ({ settled, value: settledValue }),
355
+ };
356
+ }
357
+ function enforceToolResultBatchSize(toolResults, maxChars) {
358
+ if (!maxChars || maxChars <= 0 || toolResults.length === 0) {
359
+ return toolResults;
360
+ }
361
+ const limit = Math.floor(maxChars);
362
+ let currentTotal = sumToolResultTextChars(toolResults);
363
+ if (currentTotal <= limit) {
364
+ return toolResults;
365
+ }
366
+ const next = [...toolResults];
367
+ const ranked = toolResults
368
+ .map((result, index) => ({
369
+ index,
370
+ size: sumTextContentChars(result.content),
371
+ isError: result.isError,
372
+ }))
373
+ .filter((candidate) => candidate.size > 0)
374
+ .sort((a, b) => {
375
+ if (a.isError !== b.isError)
376
+ return a.isError ? 1 : -1;
377
+ return b.size - a.size || a.index - b.index;
378
+ });
379
+ for (const candidate of ranked) {
380
+ if (currentTotal <= limit)
381
+ break;
382
+ const reductionNeeded = currentTotal - limit;
383
+ const targetSize = Math.max(0, candidate.size - reductionNeeded);
384
+ const truncated = truncateToolResultToTextChars(next[candidate.index], targetSize, limit);
385
+ next[candidate.index] = truncated.result;
386
+ currentTotal = currentTotal - candidate.size + truncated.textChars;
387
+ }
388
+ return next;
389
+ }
390
+ function truncateToolResultToTextChars(result, targetChars, maxToolResultBatchSizeChars) {
391
+ const originalTextChars = sumTextContentChars(result.content);
392
+ if (originalTextChars <= targetChars) {
393
+ return { result, textChars: originalTextChars };
394
+ }
395
+ const note = `[Tool result truncated by batch budget: original ${originalTextChars} chars.]`;
396
+ let nextText = "";
397
+ if (targetChars > 0) {
398
+ if (targetChars <= note.length) {
399
+ nextText = note.slice(0, targetChars);
400
+ }
401
+ else {
402
+ const suffix = `\n\n${note}`;
403
+ const bodyBudget = Math.max(0, targetChars - suffix.length);
404
+ nextText = `${flattenTextContent(result.content).slice(0, bodyBudget)}${suffix}`;
405
+ }
406
+ }
407
+ let hasWrittenText = false;
408
+ const content = result.content.map((part) => {
409
+ if (part.type !== "text")
410
+ return part;
411
+ if (hasWrittenText) {
412
+ return { ...part, text: "" };
413
+ }
414
+ hasWrittenText = true;
415
+ return { ...part, text: nextText };
416
+ });
417
+ if (!hasWrittenText) {
418
+ content.push({ type: "text", text: nextText });
419
+ }
420
+ return {
421
+ result: {
422
+ ...result,
423
+ content,
424
+ details: {
425
+ ...(typeof result.details === "object" && result.details !== null ? result.details : {}),
426
+ truncationReason: "tool_result_batch_budget",
427
+ originalTextChars,
428
+ retainedTextChars: nextText.length,
429
+ maxToolResultBatchSizeChars,
430
+ },
431
+ },
432
+ textChars: nextText.length,
433
+ };
434
+ }
435
+ function sumToolResultTextChars(toolResults) {
436
+ return toolResults.reduce((total, result) => total + sumTextContentChars(result.content), 0);
437
+ }
438
+ function sumTextContentChars(content) {
439
+ return content.reduce((total, part) => total + (part.type === "text" ? part.text.length : 0), 0);
440
+ }
441
+ function flattenTextContent(content) {
442
+ return content
443
+ .filter((part) => part.type === "text")
444
+ .map((part) => part.text)
445
+ .join("");
446
+ }
447
+ async function streamAssistantResponse(context, config, signal, stream, streamFn, maxTokensOverride, streamingToolExecutor) {
222
448
  let messages = context.messages;
223
449
  if (config.transformContext) {
224
450
  messages = await config.transformContext(messages, signal);
@@ -263,6 +489,16 @@ async function streamAssistantResponse(context, config, signal, stream, streamFn
263
489
  case "thinking_end":
264
490
  case "toolcall_start":
265
491
  case "toolcall_delta":
492
+ if (partialMessage) {
493
+ partialMessage = event.partial;
494
+ context.messages[context.messages.length - 1] = partialMessage;
495
+ stream.push({
496
+ type: "message_update",
497
+ assistantMessageEvent: event,
498
+ message: { ...partialMessage },
499
+ });
500
+ }
501
+ break;
266
502
  case "toolcall_end":
267
503
  if (partialMessage) {
268
504
  partialMessage = event.partial;
@@ -272,6 +508,7 @@ async function streamAssistantResponse(context, config, signal, stream, streamFn
272
508
  assistantMessageEvent: event,
273
509
  message: { ...partialMessage },
274
510
  });
511
+ streamingToolExecutor?.addTool(event.toolCall);
275
512
  }
276
513
  break;
277
514
  case "done":
@@ -313,6 +550,38 @@ function createOutputTokenRecoveryMessage(attempt) {
313
550
  timestamp: Date.now(),
314
551
  };
315
552
  }
553
+ function maybeCreateTokenBudgetContinuation(config, state) {
554
+ const budget = config.outputTokenBudget;
555
+ if (!budget)
556
+ return undefined;
557
+ const targetTokens = Math.max(1, Math.floor(budget.targetTokens));
558
+ const thresholdPct = clamp(budget.thresholdPct ?? DEFAULT_OUTPUT_TOKEN_BUDGET_THRESHOLD_PCT, 0, 1);
559
+ const maxContinuations = Math.max(0, Math.floor(budget.maxContinuations ?? DEFAULT_OUTPUT_TOKEN_BUDGET_CONTINUATIONS));
560
+ if (maxContinuations <= state.tokenBudgetContinuationCount)
561
+ return undefined;
562
+ const outputTokens = Math.max(0, Math.floor(state.usage.output));
563
+ const requiredTokens = Math.ceil(targetTokens * thresholdPct);
564
+ if (outputTokens >= requiredTokens)
565
+ return undefined;
566
+ const message = {
567
+ role: "user",
568
+ content: [
569
+ {
570
+ type: "text",
571
+ text: `Continue because the output token budget is underused ` +
572
+ `(${outputTokens}/${targetTokens} tokens). Add the missing useful detail directly; ` +
573
+ `do not recap or apologize.`,
574
+ },
575
+ ],
576
+ timestamp: Date.now(),
577
+ };
578
+ return { message, outputTokens, targetTokens };
579
+ }
580
+ function clamp(value, min, max) {
581
+ if (!Number.isFinite(value))
582
+ return min;
583
+ return Math.min(max, Math.max(min, value));
584
+ }
316
585
  function createLoopLimitMessage(config, errorMessage) {
317
586
  return {
318
587
  role: "assistant",
@@ -345,11 +614,14 @@ function endWithLoopError(stream, newMessages, config, error, signal) {
345
614
  const state = {
346
615
  turnCount: 0,
347
616
  toolCallCount: 0,
348
- transition: "start",
617
+ transition: { reason: "start" },
349
618
  pendingMessages: [],
619
+ pendingToolUseSummaries: [],
350
620
  stopHookActive: false,
351
621
  stopHookContinuationCount: 0,
352
622
  maxOutputTokensRecoveryCount: 0,
623
+ modelErrorRecoveryCount: 0,
624
+ tokenBudgetContinuationCount: 0,
353
625
  hasAttemptedReactiveCompact: false,
354
626
  startedAt: Date.now(),
355
627
  usage: emptyUsage(),
@@ -370,6 +642,7 @@ function finish(stream, newMessages, state) {
370
642
  usage: state.usage,
371
643
  permissionDenialCount: state.permissionDenials.length,
372
644
  permissionDenials: state.permissionDenials,
645
+ lastTransition: state.transition,
373
646
  errorMessage: state.finalErrorMessage,
374
647
  errorSubtype: state.finalErrorSubtype,
375
648
  });
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Structured-adaptive streaming tool executor.
3
+ * Starts complete streamed tool calls before the assistant response finishes,
4
+ * while preserving ordered tool_result emission for the next model turn.
5
+ */
6
+ /**
7
+ * [WHO]: StructuredAdaptiveStreamingToolExecutor
8
+ * [FROM]: Depends on @pencil-agent/ai, ./types, ./structured-adaptive-tool-orchestration
9
+ * [TO]: Consumed by ./structured-adaptive-agent-loop.ts
10
+ * [HERE]: packages/agent-core/src/structured-adaptive-streaming-tool-executor.ts - streaming tool scheduling for weak-model-compatible loop
11
+ */
12
+ import { EventStream } from "@pencil-agent/ai";
13
+ import type { AgentEvent, AgentLoopConfig, AgentMessage, AgentTool } from "./types.js";
14
+ import { type StructuredAdaptiveToolCall, type StructuredAdaptiveToolRunResult } from "./structured-adaptive-tool-orchestration.js";
15
+ export declare class StructuredAdaptiveStreamingToolExecutor {
16
+ private readonly stream;
17
+ private readonly canUseTool?;
18
+ private readonly toolByName;
19
+ private readonly records;
20
+ private readonly maxConcurrency;
21
+ private discardedReason;
22
+ private parentAborted;
23
+ private parentAbortReason;
24
+ constructor(tools: AgentTool<any>[] | undefined, signal: AbortSignal | undefined, stream: EventStream<AgentEvent, AgentMessage[]>, maxConcurrency: number, canUseTool?: AgentLoopConfig["canUseTool"]);
25
+ get size(): number;
26
+ addTool(toolCall: StructuredAdaptiveToolCall): void;
27
+ drain(): Promise<StructuredAdaptiveToolRunResult>;
28
+ discardAndDrain(reason: string, abortMode?: "all" | "cancel"): Promise<StructuredAdaptiveToolRunResult>;
29
+ private processQueue;
30
+ private canStart;
31
+ private start;
32
+ private completeWithSyntheticError;
33
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Structured-adaptive streaming tool executor.
3
+ * Starts complete streamed tool calls before the assistant response finishes,
4
+ * while preserving ordered tool_result emission for the next model turn.
5
+ */
6
+ /**
7
+ * [WHO]: StructuredAdaptiveStreamingToolExecutor
8
+ * [FROM]: Depends on @pencil-agent/ai, ./types, ./structured-adaptive-tool-orchestration
9
+ * [TO]: Consumed by ./structured-adaptive-agent-loop.ts
10
+ * [HERE]: packages/agent-core/src/structured-adaptive-streaming-tool-executor.ts - streaming tool scheduling for weak-model-compatible loop
11
+ */
12
+ import { buildToolMap, isStructuredAdaptiveToolCallConcurrencySafe, resolveStructuredAdaptiveToolInterruptBehavior, runStructuredAdaptiveToolUse, } from "./structured-adaptive-tool-orchestration.js";
13
+ export class StructuredAdaptiveStreamingToolExecutor {
14
+ stream;
15
+ canUseTool;
16
+ toolByName;
17
+ records = [];
18
+ maxConcurrency;
19
+ discardedReason;
20
+ parentAborted = false;
21
+ parentAbortReason;
22
+ constructor(tools, signal, stream, maxConcurrency, canUseTool) {
23
+ this.stream = stream;
24
+ this.canUseTool = canUseTool;
25
+ this.toolByName = buildToolMap(tools);
26
+ this.maxConcurrency = Math.max(1, Math.floor(maxConcurrency));
27
+ if (signal?.aborted) {
28
+ this.parentAborted = true;
29
+ this.parentAbortReason = signal.reason;
30
+ }
31
+ else {
32
+ signal?.addEventListener("abort", () => {
33
+ this.parentAborted = true;
34
+ this.parentAbortReason = signal.reason;
35
+ for (const record of this.records) {
36
+ if (record.interruptBehavior === "cancel" && !record.abortController.signal.aborted) {
37
+ record.abortController.abort(signal.reason);
38
+ }
39
+ }
40
+ }, { once: true });
41
+ }
42
+ }
43
+ get size() {
44
+ return this.records.length;
45
+ }
46
+ addTool(toolCall) {
47
+ if (this.records.some((record) => record.toolCall.id === toolCall.id))
48
+ return;
49
+ const tool = this.toolByName.get(toolCall.name);
50
+ const abortController = new AbortController();
51
+ const interruptBehavior = resolveStructuredAdaptiveToolInterruptBehavior(toolCall, tool);
52
+ if (this.parentAborted && interruptBehavior === "cancel") {
53
+ abortController.abort(this.parentAbortReason);
54
+ }
55
+ this.records.push({
56
+ toolCall,
57
+ tool,
58
+ isConcurrencySafe: isStructuredAdaptiveToolCallConcurrencySafe(toolCall, tool),
59
+ interruptBehavior,
60
+ status: "queued",
61
+ abortController,
62
+ contextMessages: [],
63
+ });
64
+ this.processQueue();
65
+ }
66
+ async drain() {
67
+ this.processQueue();
68
+ while (this.records.some((record) => record.status !== "completed")) {
69
+ const running = this.records
70
+ .filter((record) => record.status === "executing" && record.promise)
71
+ .map((record) => record.promise);
72
+ if (running.length === 0) {
73
+ this.processQueue();
74
+ continue;
75
+ }
76
+ await Promise.race(running);
77
+ this.processQueue();
78
+ }
79
+ const toolResults = this.records.map((record) => record.toolResult);
80
+ return {
81
+ toolResults,
82
+ contextMessages: this.records.flatMap((record) => record.contextMessages),
83
+ permissionDenials: extractPermissionDenials(toolResults),
84
+ };
85
+ }
86
+ async discardAndDrain(reason, abortMode = "all") {
87
+ this.discardedReason = reason;
88
+ for (const record of this.records) {
89
+ if (record.status === "executing" &&
90
+ (abortMode === "all" || record.interruptBehavior === "cancel") &&
91
+ !record.abortController.signal.aborted) {
92
+ record.abortController.abort(reason);
93
+ }
94
+ if (record.status === "queued") {
95
+ this.completeWithSyntheticError(record, reason);
96
+ }
97
+ }
98
+ return this.drain();
99
+ }
100
+ processQueue() {
101
+ for (const record of this.records) {
102
+ if (record.status !== "queued")
103
+ continue;
104
+ if (this.discardedReason) {
105
+ this.completeWithSyntheticError(record, this.discardedReason);
106
+ continue;
107
+ }
108
+ if (!this.canStart(record)) {
109
+ if (!record.isConcurrencySafe)
110
+ return;
111
+ continue;
112
+ }
113
+ this.start(record);
114
+ }
115
+ }
116
+ canStart(record) {
117
+ const executing = this.records.filter((candidate) => candidate.status === "executing");
118
+ if (record.isConcurrencySafe) {
119
+ return (executing.length < this.maxConcurrency &&
120
+ executing.every((candidate) => candidate.isConcurrencySafe));
121
+ }
122
+ return executing.length === 0;
123
+ }
124
+ start(record) {
125
+ record.status = "executing";
126
+ record.promise = runStructuredAdaptiveToolUse(record.toolCall, record.tool, record.abortController.signal, this.stream, this.canUseTool)
127
+ .then((result) => {
128
+ record.toolResult = result.toolResult;
129
+ record.contextMessages = result.contextMessages;
130
+ })
131
+ .finally(() => {
132
+ record.status = "completed";
133
+ });
134
+ }
135
+ completeWithSyntheticError(record, reason) {
136
+ const startedAt = Date.now();
137
+ const result = {
138
+ content: [{ type: "text", text: `Tool discarded because ${reason}.` }],
139
+ details: {
140
+ errorType: "streaming_tool_discarded",
141
+ reason,
142
+ toolName: record.tool?.name ?? record.toolCall.name,
143
+ toolCallId: record.toolCall.id,
144
+ },
145
+ };
146
+ this.stream.push({
147
+ type: "tool_execution_start",
148
+ toolCallId: record.toolCall.id,
149
+ toolName: record.toolCall.name,
150
+ args: record.toolCall.arguments,
151
+ });
152
+ this.stream.push({
153
+ type: "tool_execution_end",
154
+ toolCallId: record.toolCall.id,
155
+ toolName: record.toolCall.name,
156
+ result,
157
+ isError: true,
158
+ durationMs: Date.now() - startedAt,
159
+ });
160
+ record.toolResult = {
161
+ role: "toolResult",
162
+ toolCallId: record.toolCall.id,
163
+ toolName: record.toolCall.name,
164
+ content: result.content,
165
+ details: result.details,
166
+ isError: true,
167
+ timestamp: Date.now(),
168
+ };
169
+ record.contextMessages = [];
170
+ record.status = "completed";
171
+ }
172
+ }
173
+ function extractPermissionDenials(toolResults) {
174
+ const denials = [];
175
+ for (const result of toolResults) {
176
+ const details = result.details;
177
+ if (!details || typeof details !== "object")
178
+ continue;
179
+ if (details.errorType !== "permission_denied")
180
+ continue;
181
+ const reason = details.reason;
182
+ denials.push({
183
+ toolCallId: result.toolCallId,
184
+ toolName: result.toolName,
185
+ reason: typeof reason === "string" && reason.length > 0 ? reason : undefined,
186
+ });
187
+ }
188
+ return denials;
189
+ }
@@ -20,6 +20,15 @@ export interface StructuredAdaptiveToolRunResult {
20
20
  steeringMessages?: AgentMessage[];
21
21
  permissionDenials: AgentToolPermissionDenial[];
22
22
  }
23
+ interface StructuredAdaptiveToolUseResult {
24
+ toolResult: ToolResultMessage;
25
+ contextMessages: AgentMessage[];
26
+ }
23
27
  export declare function runStructuredAdaptiveTools(toolCalls: StructuredAdaptiveToolCall[], tools: AgentTool<any>[] | undefined, signal: AbortSignal | undefined, stream: EventStream<AgentEvent, AgentMessage[]>, getSteeringMessages?: AgentLoopConfig["getSteeringMessages"], maxConcurrency?: number, canUseTool?: AgentLoopConfig["canUseTool"]): Promise<StructuredAdaptiveToolRunResult>;
28
+ export declare function resolveMaxToolConcurrency(maxConcurrency: number | undefined): number;
24
29
  export declare function buildToolMap(tools: AgentTool<any>[] | undefined): Map<string, AgentTool<any>>;
25
30
  export declare function partitionStructuredAdaptiveToolCalls(toolCalls: StructuredAdaptiveToolCall[], toolByName: Map<string, AgentTool<any>>): StructuredAdaptiveToolCall[][];
31
+ export declare function isStructuredAdaptiveToolCallConcurrencySafe(toolCall: StructuredAdaptiveToolCall, tool: AgentTool<any> | undefined): boolean;
32
+ export declare function resolveStructuredAdaptiveToolInterruptBehavior(toolCall: StructuredAdaptiveToolCall, tool: AgentTool<any> | undefined): "cancel" | "block";
33
+ export declare function runStructuredAdaptiveToolUse(toolCall: StructuredAdaptiveToolCall, tool: AgentTool<any> | undefined, signal: AbortSignal | undefined, stream: EventStream<AgentEvent, AgentMessage[]>, canUseTool?: AgentLoopConfig["canUseTool"]): Promise<StructuredAdaptiveToolUseResult>;
34
+ export {};