@pencil-agent/nano-pencil 1.14.4 → 1.14.6

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 (64) hide show
  1. package/dist/build-meta.json +3 -3
  2. package/dist/cli/args.d.ts +11 -1
  3. package/dist/cli/args.js +130 -0
  4. package/dist/core/extensions/index.d.ts +1 -1
  5. package/dist/core/extensions/types.d.ts +7 -2
  6. package/dist/core/runtime/agent-session.d.ts +5 -2
  7. package/dist/core/runtime/agent-session.js +39 -7
  8. package/dist/core/runtime/sdk.d.ts +21 -1
  9. package/dist/core/runtime/sdk.js +12 -3
  10. package/dist/extensions/defaults/AGENT.md +3 -3
  11. package/dist/extensions/defaults/CLAUDE.md +1 -1
  12. package/dist/extensions/defaults/sal/index.d.ts +2 -2
  13. package/dist/extensions/defaults/sal/index.js +16 -2
  14. package/dist/extensions/defaults/sal/sal-runtime.d.ts +3 -1
  15. package/dist/extensions/defaults/sal/sal-runtime.js +1 -1
  16. package/dist/extensions/defaults/sal/sal-trace.d.ts +1 -1
  17. package/dist/extensions/defaults/sal/sal-trace.js +13 -2
  18. package/dist/main.js +11 -2
  19. package/dist/modes/acp/acp-mode.d.ts +3 -0
  20. package/dist/modes/acp/acp-mode.js +40 -1
  21. package/dist/modes/agent-loop-result-format.d.ts +10 -0
  22. package/dist/modes/agent-loop-result-format.js +69 -0
  23. package/dist/modes/interactive/agent-loop-status.d.ts +8 -0
  24. package/dist/modes/interactive/agent-loop-status.js +41 -0
  25. package/dist/modes/interactive/interactive-mode.js +12 -21
  26. package/dist/modes/interactive/slash-command-arguments.d.ts +13 -1
  27. package/dist/modes/interactive/slash-command-arguments.js +34 -5
  28. package/dist/modes/print-mode.d.ts +17 -5
  29. package/dist/modes/print-mode.js +77 -5
  30. package/dist/modes/rpc/rpc-client.d.ts +5 -1
  31. package/dist/modes/rpc/rpc-client.js +6 -0
  32. package/dist/modes/rpc/rpc-mode.d.ts +6 -2
  33. package/dist/modes/rpc/rpc-mode.js +52 -17
  34. package/dist/modes/rpc/rpc-types.d.ts +21 -1
  35. package/dist/node_modules/@pencil-agent/agent-core/agent-loop-continuations.d.ts +17 -0
  36. package/dist/node_modules/@pencil-agent/agent-core/agent-loop-continuations.js +60 -0
  37. package/dist/node_modules/@pencil-agent/agent-core/agent-loop-stream-events.d.ts +18 -0
  38. package/dist/node_modules/@pencil-agent/agent-core/agent-loop-stream-events.js +55 -0
  39. package/dist/node_modules/@pencil-agent/agent-core/agent-loop-tool-results.d.ts +10 -0
  40. package/dist/node_modules/@pencil-agent/agent-core/agent-loop-tool-results.js +137 -0
  41. package/dist/node_modules/@pencil-agent/agent-core/agent-loop-tool-summaries.d.ts +22 -0
  42. package/dist/node_modules/@pencil-agent/agent-core/agent-loop-tool-summaries.js +64 -0
  43. package/dist/node_modules/@pencil-agent/agent-core/agent-loop.d.ts +4 -4
  44. package/dist/node_modules/@pencil-agent/agent-core/agent-loop.js +571 -48
  45. package/dist/node_modules/@pencil-agent/agent-core/agent-run-result.d.ts +9 -0
  46. package/dist/node_modules/@pencil-agent/agent-core/agent-run-result.js +32 -0
  47. package/dist/node_modules/@pencil-agent/agent-core/agent.d.ts +30 -4
  48. package/dist/node_modules/@pencil-agent/agent-core/agent.js +75 -2
  49. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-agent-loop.d.ts +2 -2
  50. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-agent-loop.js +178 -237
  51. package/dist/node_modules/@pencil-agent/agent-core/types.d.ts +60 -27
  52. package/dist/node_modules/@pencil-agent/agent-core/types.js +1 -1
  53. package/dist/node_modules/@pencil-agent/ai/models.generated.d.ts +34 -17
  54. package/dist/node_modules/@pencil-agent/ai/models.generated.js +48 -31
  55. package/dist/node_modules/@pencil-agent/ai/providers/transform-messages.d.ts +3 -3
  56. package/dist/node_modules/@pencil-agent/ai/providers/transform-messages.js +42 -27
  57. package/dist/node_modules/@pencil-agent/ai/stream.d.ts +2 -2
  58. package/dist/node_modules/@pencil-agent/ai/stream.js +155 -25
  59. package/dist/node_modules/@pencil-agent/ai/utils/event-stream.d.ts +5 -1
  60. package/dist/node_modules/@pencil-agent/ai/utils/event-stream.js +15 -3
  61. package/dist/node_modules/@pencil-agent/tui/tui.d.ts +3 -8
  62. package/dist/node_modules/@pencil-agent/tui/tui.js +33 -70
  63. package/docs/agent-loop-frameworks.md +26 -3
  64. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.14.4",
3
- "commitHash": "092b256",
2
+ "version": "1.14.6",
3
+ "commitHash": "111a12b",
4
4
  "branch": "main",
5
- "builtAt": "2026-05-27T16:26:12.388Z"
5
+ "builtAt": "2026-05-28T14:39:06.773Z"
6
6
  }
@@ -4,7 +4,7 @@
4
4
  * [TO]: Consumed by main.ts, core/model-resolver.ts
5
5
  * [HERE]: cli/args.ts - CLI argument parsing and help display
6
6
  */
7
- import type { ThinkingLevel } from "@pencil-agent/agent-core";
7
+ import { type AgentLoopFrameworkInput, type AgentLoopPolicyOptions, type ThinkingLevel } from "@pencil-agent/agent-core";
8
8
  import { type ToolName } from "../core/tools/index.js";
9
9
  export type Mode = "text" | "json" | "rpc";
10
10
  export interface Args {
@@ -29,6 +29,16 @@ export interface Args {
29
29
  extensions?: string[];
30
30
  noExtensions?: boolean;
31
31
  print?: boolean;
32
+ /** In text print mode, emit the final agent loop result as stderr JSON. */
33
+ printLoopResult?: boolean;
34
+ /** In print mode, exit non-zero when the final agent result is an error. */
35
+ failOnAgentError?: boolean;
36
+ /** In print mode, exit non-zero when any tool permission denial occurred. */
37
+ failOnToolDenial?: boolean;
38
+ /** Non-persistent agent loop framework override for this process/session. */
39
+ agentLoopFramework?: AgentLoopFrameworkInput;
40
+ /** Non-persistent loop policy overrides for this process/session. */
41
+ loopPolicy?: Pick<AgentLoopPolicyOptions, "maxTurnsPerPrompt" | "maxToolCallsPerPrompt" | "maxToolConcurrency" | "maxToolResultBatchSizeChars" | "outputTokenBudget" | "maxOutputTokenRecoveryAttempts" | "maxModelErrorRecoveryAttempts" | "maxStopHookContinuations">;
32
42
  export?: string;
33
43
  noSkills?: boolean;
34
44
  skills?: string[];
package/dist/cli/args.js CHANGED
@@ -1,3 +1,10 @@
1
+ /**
2
+ * [WHO]: Args, Mode, parseArgs(), printHelp()
3
+ * [FROM]: Depends on agent-core, chalk, config.ts, core/tools
4
+ * [TO]: Consumed by main.ts, core/model-resolver.ts
5
+ * [HERE]: cli/args.ts - CLI argument parsing and help display
6
+ */
7
+ import { normalizeAgentLoopFramework, } from "@pencil-agent/agent-core";
1
8
  import chalk from "chalk";
2
9
  import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR } from "../config.js";
3
10
  import { allTools } from "../core/tools/index.js";
@@ -5,12 +12,45 @@ const VALID_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh
5
12
  export function isValidThinkingLevel(level) {
6
13
  return VALID_THINKING_LEVELS.includes(level);
7
14
  }
15
+ function parseAgentLoopFramework(value) {
16
+ const normalized = normalizeAgentLoopFramework(value);
17
+ if (normalized === "standard" || normalized === "weak-model-compatible") {
18
+ return normalized;
19
+ }
20
+ return undefined;
21
+ }
22
+ function parsePositiveIntegerOption(flag, value) {
23
+ const parsed = Number(value);
24
+ if (Number.isInteger(parsed) && parsed > 0)
25
+ return parsed;
26
+ console.error(chalk.yellow(`Warning: Invalid ${flag} value "${value}". Expected a positive integer.`));
27
+ return undefined;
28
+ }
29
+ function parseNonNegativeIntegerOption(flag, value) {
30
+ const parsed = Number(value);
31
+ if (Number.isInteger(parsed) && parsed >= 0)
32
+ return parsed;
33
+ console.error(chalk.yellow(`Warning: Invalid ${flag} value "${value}". Expected a non-negative integer.`));
34
+ return undefined;
35
+ }
36
+ function parseUnitIntervalOption(flag, value) {
37
+ const parsed = Number(value);
38
+ if (Number.isFinite(parsed) && parsed > 0 && parsed <= 1)
39
+ return parsed;
40
+ console.error(chalk.yellow(`Warning: Invalid ${flag} value "${value}". Expected a number in (0, 1].`));
41
+ return undefined;
42
+ }
43
+ function setLoopPolicyOption(result, key, value) {
44
+ result.loopPolicy = result.loopPolicy ?? {};
45
+ result.loopPolicy[key] = value;
46
+ }
8
47
  export function parseArgs(args, extensionFlags) {
9
48
  const result = {
10
49
  messages: [],
11
50
  fileArgs: [],
12
51
  unknownFlags: new Map(),
13
52
  };
53
+ const outputTokenBudget = {};
14
54
  for (let i = 0; i < args.length; i++) {
15
55
  const arg = args[i];
16
56
  if (arg === "--help" || arg === "-h") {
@@ -89,6 +129,75 @@ export function parseArgs(args, extensionFlags) {
89
129
  else if (arg === "--print" || arg === "-p") {
90
130
  result.print = true;
91
131
  }
132
+ else if (arg === "--print-loop-result") {
133
+ result.printLoopResult = true;
134
+ }
135
+ else if (arg === "--fail-on-agent-error") {
136
+ result.failOnAgentError = true;
137
+ }
138
+ else if (arg === "--fail-on-tool-denial") {
139
+ result.failOnToolDenial = true;
140
+ }
141
+ else if (arg === "--agent-loop" && i + 1 < args.length) {
142
+ const framework = args[++i];
143
+ const normalized = parseAgentLoopFramework(framework);
144
+ if (normalized) {
145
+ result.agentLoopFramework = normalized;
146
+ }
147
+ else {
148
+ console.error(chalk.yellow(`Warning: Invalid agent loop framework "${framework}". Valid values: standard, weak-model-compatible.`));
149
+ }
150
+ }
151
+ else if (arg === "--max-turns-per-prompt" && i + 1 < args.length) {
152
+ const value = parsePositiveIntegerOption(arg, args[++i]);
153
+ if (value !== undefined)
154
+ setLoopPolicyOption(result, "maxTurnsPerPrompt", value);
155
+ }
156
+ else if (arg === "--max-tool-calls-per-prompt" && i + 1 < args.length) {
157
+ const value = parsePositiveIntegerOption(arg, args[++i]);
158
+ if (value !== undefined)
159
+ setLoopPolicyOption(result, "maxToolCallsPerPrompt", value);
160
+ }
161
+ else if (arg === "--max-tool-concurrency" && i + 1 < args.length) {
162
+ const value = parsePositiveIntegerOption(arg, args[++i]);
163
+ if (value !== undefined)
164
+ setLoopPolicyOption(result, "maxToolConcurrency", value);
165
+ }
166
+ else if (arg === "--max-tool-result-batch-size-chars" && i + 1 < args.length) {
167
+ const value = parsePositiveIntegerOption(arg, args[++i]);
168
+ if (value !== undefined)
169
+ setLoopPolicyOption(result, "maxToolResultBatchSizeChars", value);
170
+ }
171
+ else if (arg === "--output-token-budget" && i + 1 < args.length) {
172
+ const value = parsePositiveIntegerOption(arg, args[++i]);
173
+ if (value !== undefined)
174
+ outputTokenBudget.targetTokens = value;
175
+ }
176
+ else if (arg === "--output-token-budget-threshold" && i + 1 < args.length) {
177
+ const value = parseUnitIntervalOption(arg, args[++i]);
178
+ if (value !== undefined)
179
+ outputTokenBudget.thresholdPct = value;
180
+ }
181
+ else if (arg === "--output-token-budget-continuations" && i + 1 < args.length) {
182
+ const value = parseNonNegativeIntegerOption(arg, args[++i]);
183
+ if (value !== undefined)
184
+ outputTokenBudget.maxContinuations = value;
185
+ }
186
+ else if (arg === "--max-output-token-recovery-attempts" && i + 1 < args.length) {
187
+ const value = parseNonNegativeIntegerOption(arg, args[++i]);
188
+ if (value !== undefined)
189
+ setLoopPolicyOption(result, "maxOutputTokenRecoveryAttempts", value);
190
+ }
191
+ else if (arg === "--max-model-error-recovery-attempts" && i + 1 < args.length) {
192
+ const value = parseNonNegativeIntegerOption(arg, args[++i]);
193
+ if (value !== undefined)
194
+ setLoopPolicyOption(result, "maxModelErrorRecoveryAttempts", value);
195
+ }
196
+ else if (arg === "--max-stop-hook-continuations" && i + 1 < args.length) {
197
+ const value = parseNonNegativeIntegerOption(arg, args[++i]);
198
+ if (value !== undefined)
199
+ setLoopPolicyOption(result, "maxStopHookContinuations", value);
200
+ }
92
201
  else if (arg === "--export" && i + 1 < args.length) {
93
202
  result.export = args[++i];
94
203
  }
@@ -168,6 +277,13 @@ export function parseArgs(args, extensionFlags) {
168
277
  result.messages.push(arg);
169
278
  }
170
279
  }
280
+ if (outputTokenBudget.targetTokens !== undefined) {
281
+ setLoopPolicyOption(result, "outputTokenBudget", {
282
+ targetTokens: outputTokenBudget.targetTokens,
283
+ thresholdPct: outputTokenBudget.thresholdPct,
284
+ maxContinuations: outputTokenBudget.maxContinuations,
285
+ });
286
+ }
171
287
  return result;
172
288
  }
173
289
  export function printHelp() {
@@ -194,6 +310,20 @@ ${chalk.bold("Options:")}
194
310
  --append-system-prompt <text> Append text or file contents to the system prompt
195
311
  --mode <mode> Output mode: text (default), json, or rpc
196
312
  --print, -p Non-interactive mode: process prompt and exit
313
+ --print-loop-result In text print mode, write final loop result JSON to stderr
314
+ --fail-on-agent-error In print mode, exit non-zero when final loop result is an error
315
+ --fail-on-tool-denial In print mode, exit non-zero when tools were denied
316
+ --agent-loop <framework> Override loop framework: standard or weak-model-compatible
317
+ --max-turns-per-prompt <n> Stop a prompt after n assistant turns
318
+ --max-tool-calls-per-prompt <n> Stop a prompt after n tool calls
319
+ --max-tool-concurrency <n> Max concurrent safe tool calls in compatible loop
320
+ --max-tool-result-batch-size-chars <n> Max aggregate tool result chars per turn
321
+ --output-token-budget <n> Continue when final output is below n tokens
322
+ --output-token-budget-threshold <n> Continuation threshold ratio in (0,1], default loop policy
323
+ --output-token-budget-continuations <n> Max output-budget continuations
324
+ --max-output-token-recovery-attempts <n> Max recovery turns after output-token stops
325
+ --max-model-error-recovery-attempts <n> Max in-loop model error recoveries
326
+ --max-stop-hook-continuations <n> Max stop-hook validation continuations
197
327
  --continue, -c Continue previous session
198
328
  --resume, -r Select a session to resume
199
329
  --session <path> Use specific session file
@@ -8,6 +8,6 @@ export type { SlashCommandInfo, SlashCommandLocation, SlashCommandSource } from
8
8
  export { createExtensionRuntime, discoverAndLoadExtensions, loadExtensionFromFactory, loadExtensions, } from "./loader.js";
9
9
  export type { ExtensionErrorListener, ForkHandler, NavigateTreeHandler, NewSessionHandler, ShutdownHandler, SwitchSessionHandler, } from "./runner.js";
10
10
  export { ExtensionRunner } from "./runner.js";
11
- export type { AgentEndEvent, AgentStartEvent, AgentToolResult, AgentToolUpdateCallback, AppAction, AppendEntryHandler, BashToolCallEvent, BashToolResultEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, CompactOptions, ContextEvent, ContextEventResult, ContextUsage, CustomToolCallEvent, CustomToolResultEvent, EditToolCallEvent, EditToolResultEvent, ExecOptions, ExecResult, Extension, ExtensionActions, ExtensionAPI, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFactory, ExtensionFlag, ExtensionHandler, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, ExtensionUIDialogOptions, ExtensionWidgetOptions, FindToolCallEvent, FindToolResultEvent, GetActiveToolsHandler, GetAllToolsHandler, GetCommandsHandler, GetThinkingLevelHandler, GrepToolCallEvent, GrepToolResultEvent, InputEvent, InputEventResult, InputSource, KeybindingsManager, LoadExtensionsResult, LsToolCallEvent, LsToolResultEvent, MessageEndEvent, MessageRenderer, MessageRenderOptions, MessageStartEvent, MessageUpdateEvent, ModelSelectEvent, ModelSelectSource, ProviderConfig, ProviderModelConfig, ReadToolCallEvent, ReadToolResultEvent, RegisteredCommand, RegisteredTool, ResourcesDiscoverEvent, ResourcesDiscoverResult, SendMessageHandler, SendUserMessageHandler, SessionBeforeCompactEvent, SessionBeforeCompactResult, SessionBeforeForkEvent, SessionBeforeForkResult, SessionBeforeSwitchEvent, SessionBeforeSwitchResult, SessionBeforeTreeEvent, SessionBeforeTreeResult, SessionCompactEvent, SessionEvent, SessionForkEvent, SessionShutdownEvent, SessionStartEvent, SessionSwitchEvent, SessionTreeEvent, SetActiveToolsHandler, SetLabelHandler, SetModelHandler, SetThinkingLevelHandler, TerminalInputHandler, ToolCallEvent, ToolCallEventResult, ToolDefinition, ToolExecutionEndEvent, ToolExecutionStartEvent, ToolExecutionUpdateEvent, ToolInfo, ToolRenderResultOptions, ToolResultEvent, ToolResultEventResult, TreePreparation, TurnEndEvent, TurnStartEvent, UserBashEvent, UserBashEventResult, WidgetPlacement, WriteToolCallEvent, WriteToolResultEvent, } from "./types.js";
11
+ export type { AgentEndEvent, AgentResultEvent, AgentStartEvent, AgentToolResult, AgentToolUpdateCallback, AppAction, AppendEntryHandler, BashToolCallEvent, BashToolResultEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, CompactOptions, ContextEvent, ContextEventResult, ContextUsage, CustomToolCallEvent, CustomToolResultEvent, EditToolCallEvent, EditToolResultEvent, ExecOptions, ExecResult, Extension, ExtensionActions, ExtensionAPI, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFactory, ExtensionFlag, ExtensionHandler, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, ExtensionUIDialogOptions, ExtensionWidgetOptions, FindToolCallEvent, FindToolResultEvent, GetActiveToolsHandler, GetAllToolsHandler, GetCommandsHandler, GetThinkingLevelHandler, GrepToolCallEvent, GrepToolResultEvent, InputEvent, InputEventResult, InputSource, KeybindingsManager, LoadExtensionsResult, LsToolCallEvent, LsToolResultEvent, MessageEndEvent, MessageRenderer, MessageRenderOptions, MessageStartEvent, MessageUpdateEvent, ModelSelectEvent, ModelSelectSource, ProviderConfig, ProviderModelConfig, ReadToolCallEvent, ReadToolResultEvent, RegisteredCommand, RegisteredTool, ResourcesDiscoverEvent, ResourcesDiscoverResult, SendMessageHandler, SendUserMessageHandler, SessionBeforeCompactEvent, SessionBeforeCompactResult, SessionBeforeForkEvent, SessionBeforeForkResult, SessionBeforeSwitchEvent, SessionBeforeSwitchResult, SessionBeforeTreeEvent, SessionBeforeTreeResult, SessionCompactEvent, SessionEvent, SessionForkEvent, SessionShutdownEvent, SessionStartEvent, SessionSwitchEvent, SessionTreeEvent, SetActiveToolsHandler, SetLabelHandler, SetModelHandler, SetThinkingLevelHandler, TerminalInputHandler, ToolCallEvent, ToolCallEventResult, ToolDefinition, ToolExecutionEndEvent, ToolExecutionStartEvent, ToolExecutionUpdateEvent, ToolInfo, ToolRenderResultOptions, ToolResultEvent, ToolResultEventResult, TreePreparation, TurnEndEvent, TurnStartEvent, UserBashEvent, UserBashEventResult, WidgetPlacement, WriteToolCallEvent, WriteToolResultEvent, } from "./types.js";
12
12
  export { isBashToolResult, isEditToolResult, isFindToolResult, isGrepToolResult, isLsToolResult, isReadToolResult, isToolCallEventType, isWriteToolResult, } from "./types.js";
13
13
  export { wrapRegisteredTool, wrapRegisteredTools, wrapToolsWithExtensions, wrapToolWithExtensions, } from "./wrapper.js";
@@ -4,7 +4,7 @@
4
4
  * [TO]: Consumed by core/extensions/index.ts, core/extensions/runner.ts, core/extensions/wrapper.ts, all extension entry points (defaults/loop, defaults/team, defaults/mcp, defaults/soul, defaults/presence, defaults/security-audit, defaults/link-world, defaults/interview, optional/simplify, optional/export-html), modes/interactive/components/tool-execution.ts, modes/interactive/components/custom-message.ts, modes/acp/acp-mode.ts
5
5
  * [HERE]: core/extensions/types.ts - type definitions for extension system API
6
6
  */
7
- import type { AgentMessage, AgentToolResult, AgentToolUpdateCallback, AgentLoopFramework, ThinkingLevel } from "@pencil-agent/agent-core";
7
+ import type { AgentMessage, AgentRunResult, AgentToolResult, AgentToolUpdateCallback, AgentLoopFramework, ThinkingLevel } from "@pencil-agent/agent-core";
8
8
  import type { Api, AssistantMessageEvent, AssistantMessageEventStream, Context, ImageContent, Model, OAuthCredentials, OAuthLoginCallbacks, SimpleStreamOptions, TextContent, ToolResultMessage, Usage } from "@pencil-agent/ai";
9
9
  import type { AutocompleteItem, Component, EditorComponent, EditorTheme, KeyId, OverlayHandle, OverlayOptions, TUI } from "@pencil-agent/tui";
10
10
  import type { Static, TSchema } from "@sinclair/typebox";
@@ -418,6 +418,10 @@ export interface AgentEndEvent {
418
418
  type: "agent_end";
419
419
  messages: AgentMessage[];
420
420
  }
421
+ /** Fired with structured loop outcome metadata before agent_end */
422
+ export interface AgentResultEvent extends AgentRunResult {
423
+ type: "agent_result";
424
+ }
421
425
  /** Fired at the start of each turn */
422
426
  export interface TurnStartEvent {
423
427
  type: "turn_start";
@@ -628,7 +632,7 @@ export declare function isToolCallEventType<TName extends string, TInput extends
628
632
  input: TInput;
629
633
  };
630
634
  /** Union of all event types */
631
- export type ExtensionEvent = ResourcesDiscoverEvent | SessionEvent | ContextEvent | BeforeAgentStartEvent | AgentStartEvent | AgentEndEvent | TurnStartEvent | TurnEndEvent | MessageStartEvent | MessageUpdateEvent | MessageEndEvent | ToolExecutionStartEvent | ToolExecutionUpdateEvent | ToolExecutionEndEvent | ModelSelectEvent | UserBashEvent | InputEvent | ToolCallEvent | ToolResultEvent;
635
+ export type ExtensionEvent = ResourcesDiscoverEvent | SessionEvent | ContextEvent | BeforeAgentStartEvent | AgentStartEvent | AgentEndEvent | AgentResultEvent | TurnStartEvent | TurnEndEvent | MessageStartEvent | MessageUpdateEvent | MessageEndEvent | ToolExecutionStartEvent | ToolExecutionUpdateEvent | ToolExecutionEndEvent | ModelSelectEvent | UserBashEvent | InputEvent | ToolCallEvent | ToolResultEvent;
632
636
  export interface ContextEventResult {
633
637
  messages?: AgentMessage[];
634
638
  }
@@ -723,6 +727,7 @@ export interface ExtensionAPI {
723
727
  on(event: "context", handler: ExtensionHandler<ContextEvent, ContextEventResult>): void;
724
728
  on(event: "before_agent_start", handler: ExtensionHandler<BeforeAgentStartEvent, BeforeAgentStartEventResult>): void;
725
729
  on(event: "agent_start", handler: ExtensionHandler<AgentStartEvent>): void;
730
+ on(event: "agent_result", handler: ExtensionHandler<AgentResultEvent>): void;
726
731
  on(event: "agent_end", handler: ExtensionHandler<AgentEndEvent>): void;
727
732
  on(event: "turn_start", handler: ExtensionHandler<TurnStartEvent>): void;
728
733
  on(event: "turn_end", handler: ExtensionHandler<TurnEndEvent>): void;
@@ -1,5 +1,5 @@
1
- import type { Agent, AgentEvent, AgentLoopFramework, AgentMessage, AgentState, AgentTool, ThinkingLevel } from "@pencil-agent/agent-core";
2
- import type { ImageContent, Model, TextContent } from "@pencil-agent/ai";
1
+ import type { Agent, AgentEvent, AgentLoopFramework, AgentLoopPolicyOptions, AgentMessage, AgentState, AgentTool, ThinkingLevel } from "@pencil-agent/agent-core";
2
+ import type { AssistantMessage, ImageContent, Model, TextContent } from "@pencil-agent/ai";
3
3
  /**
4
4
  * Custom error for model cycling with additional context.
5
5
  */
@@ -37,6 +37,7 @@ export interface ParsedSkillBlock {
37
37
  * Returns null if the text doesn't contain a skill block.
38
38
  */
39
39
  export declare function parseSkillBlock(text: string): ParsedSkillBlock | null;
40
+ export declare function pruneRecoverableErrorTail(messages: AgentMessage[], assistantMessage: AssistantMessage): AgentMessage[];
40
41
  /** Session-specific events that extend the core AgentEvent */
41
42
  export type AgentSessionEvent = AgentEvent | {
42
43
  type: "auto_compaction_start";
@@ -465,6 +466,8 @@ export declare class AgentSession {
465
466
  setThinkingLevel(level: ThinkingLevel): void;
466
467
  /** Set the session-level agent loop framework override. */
467
468
  setAgentLoopFramework(framework: AgentLoopFrameworkInput | undefined): void;
469
+ /** Update runtime loop policy options for subsequent turns. */
470
+ setLoopPolicy(options: Partial<AgentLoopPolicyOptions>): void;
468
471
  /**
469
472
  * Cycle to next thinking level.
470
473
  * @returns New level, or undefined if model doesn't support thinking
@@ -1,5 +1,5 @@
1
1
  /**
2
- * [WHO]: AgentSession class, session lifecycle, event emission, in-loop context-overflow recovery adapter
2
+ * [WHO]: AgentSession class, session lifecycle, event emission, in-loop recovery adapter, pruneRecoverableErrorTail()
3
3
  * [FROM]: Depends on agent-core, ai, core/tools/*, core/session/*, core/config/*
4
4
  * [TO]: Consumed by core/index.ts, core/runtime/sdk.ts, modes/interactive/interactive-mode.ts, modes/print-mode.ts, modes/rpc/rpc-mode.ts, modes/acp/acp-mode.ts, modes/rpc/rpc-types.ts, modes/rpc/rpc-client.ts, modes/interactive/components/footer.ts, modes/interactive/components/skill-invocation-message.ts
5
5
  * [HERE]: Central runtime hub; all modes delegate to this class
@@ -58,6 +58,34 @@ export function parseSkillBlock(text) {
58
58
  userMessage: match[4]?.trim() || undefined,
59
59
  };
60
60
  }
61
+ export function pruneRecoverableErrorTail(messages, assistantMessage) {
62
+ const interruptedToolCallIds = new Set(assistantMessage.content
63
+ .filter((part) => part.type === "toolCall")
64
+ .map((part) => part.id));
65
+ let end = messages.length;
66
+ while (end > 0 &&
67
+ isRecoverableTailToolResult(messages[end - 1], interruptedToolCallIds)) {
68
+ end--;
69
+ }
70
+ if (end > 0 &&
71
+ isSameRecoverableAssistantMessage(messages[end - 1], assistantMessage)) {
72
+ end--;
73
+ }
74
+ return messages.slice(0, end);
75
+ }
76
+ function isRecoverableTailToolResult(message, interruptedToolCallIds) {
77
+ return (message.role === "toolResult" &&
78
+ interruptedToolCallIds.has(message.toolCallId));
79
+ }
80
+ function isSameRecoverableAssistantMessage(message, assistantMessage) {
81
+ return (message.role === "assistant" &&
82
+ message.stopReason === assistantMessage.stopReason &&
83
+ message.timestamp === assistantMessage.timestamp &&
84
+ message.provider === assistantMessage.provider &&
85
+ message.model === assistantMessage.model &&
86
+ message.api === assistantMessage.api &&
87
+ message.errorMessage === assistantMessage.errorMessage);
88
+ }
61
89
  // ============================================================================
62
90
  // Constants
63
91
  // ============================================================================
@@ -432,6 +460,10 @@ export class AgentSession {
432
460
  this._turnIndex = 0;
433
461
  await this._extensionRunner.emit({ type: "agent_start" });
434
462
  }
463
+ else if (event.type === "agent_result") {
464
+ const extensionEvent = { ...event };
465
+ await this._extensionRunner.emit(extensionEvent);
466
+ }
435
467
  else if (event.type === "turn_start") {
436
468
  const extensionEvent = {
437
469
  type: "turn_start",
@@ -1334,6 +1366,10 @@ export class AgentSession {
1334
1366
  setAgentLoopFramework(framework) {
1335
1367
  this.agent.setAgentLoopFramework(framework);
1336
1368
  }
1369
+ /** Update runtime loop policy options for subsequent turns. */
1370
+ setLoopPolicy(options) {
1371
+ this.agent.setLoopPolicy(options);
1372
+ }
1337
1373
  /**
1338
1374
  * Cycle to next thinking level.
1339
1375
  * @returns New level, or undefined if model doesn't support thinking
@@ -1589,8 +1625,7 @@ export class AgentSession {
1589
1625
  const shouldRetry = await this._retryCoordinator.handleErrorInLoop(assistantMessage);
1590
1626
  if (!shouldRetry)
1591
1627
  return { action: "stop" };
1592
- const messages = this.agent.state.messages;
1593
- const retryMessages = messages.at(-1)?.role === "assistant" ? messages.slice(0, -1) : messages;
1628
+ const retryMessages = pruneRecoverableErrorTail(this.agent.state.messages, assistantMessage);
1594
1629
  this.agent.replaceMessages(retryMessages);
1595
1630
  return {
1596
1631
  action: "retry",
@@ -1617,10 +1652,7 @@ export class AgentSession {
1617
1652
  if (errorIsFromBeforeCompaction)
1618
1653
  return { action: "stop" };
1619
1654
  const messages = this.agent.state.messages;
1620
- if (messages.length > 0 &&
1621
- messages[messages.length - 1].role === "assistant") {
1622
- this.agent.replaceMessages(messages.slice(0, -1));
1623
- }
1655
+ this.agent.replaceMessages(pruneRecoverableErrorTail(messages, assistantMessage));
1624
1656
  const recoveredMessages = await this._runAutoCompaction("overflow", true, {
1625
1657
  triggerContinue: false,
1626
1658
  });
@@ -1,4 +1,4 @@
1
- import { type ThinkingLevel } from "@pencil-agent/agent-core";
1
+ import { type AgentLoopFrameworkInput, type AgentLoopPolicyOptions, type ThinkingLevel } from "@pencil-agent/agent-core";
2
2
  import type { Model } from "@pencil-agent/ai";
3
3
  import { AgentSession } from "./agent-session.js";
4
4
  import { AuthStorage } from "../config/auth-storage.js";
@@ -41,6 +41,26 @@ export interface CreateAgentSessionOptions {
41
41
  model?: Model<any>;
42
42
  /** Thinking level. Default: from settings, else 'medium' (clamped to model capabilities) */
43
43
  thinkingLevel?: ThinkingLevel;
44
+ /** Session-level agent loop framework override. Default: from settings/model. */
45
+ agentLoopFramework?: AgentLoopFrameworkInput;
46
+ /** Optional runtime loop policy overrides applied at session creation. */
47
+ loopPolicy?: Pick<AgentLoopPolicyOptions, "maxTurnsPerPrompt" | "maxToolCallsPerPrompt" | "maxToolConcurrency" | "maxToolResultBatchSizeChars" | "outputTokenBudget" | "maxOutputTokenRecoveryAttempts" | "maxModelErrorRecoveryAttempts" | "maxStopHookContinuations">;
48
+ /** Maximum assistant turns allowed for one prompt. */
49
+ maxTurnsPerPrompt?: number;
50
+ /** Maximum tool calls allowed for one prompt. */
51
+ maxToolCallsPerPrompt?: number;
52
+ /** Maximum concurrent safe tool calls in compatible loops. */
53
+ maxToolConcurrency?: number;
54
+ /** Aggregate tool-result batch budget in characters. */
55
+ maxToolResultBatchSizeChars?: number;
56
+ /** Optional target for automatic continuation when output is under-complete. */
57
+ outputTokenBudget?: AgentLoopPolicyOptions["outputTokenBudget"];
58
+ /** Maximum automatic output-token recovery turns per prompt. */
59
+ maxOutputTokenRecoveryAttempts?: number;
60
+ /** Maximum in-loop model error recoveries per prompt. */
61
+ maxModelErrorRecoveryAttempts?: number;
62
+ /** Maximum stop-hook validation/correction continuations per prompt. */
63
+ maxStopHookContinuations?: number;
44
64
  /** Models available for cycling (Ctrl+P in interactive mode) */
45
65
  scopedModels?: Array<{
46
66
  model: Model<any>;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * [WHO]: createAgentSession(options) → AgentSession + load results, runtime loop setting wiring
2
+ * [WHO]: createAgentSession(options) → AgentSession + load results, loop framework/policy override wiring
3
3
  * [FROM]: Depends on agent-core, ai, core/config/*, core/tools/*, core/session/*, core/mcp-*, i18n/*
4
4
  * [TO]: Consumed by index.ts, main.ts, test/presence-opening.test.ts, extensions/defaults/team/index.ts
5
5
  * [HERE]: SDK factory; creates all services with DI, wires up extensions
@@ -247,10 +247,19 @@ export async function createAgentSession(options = {}) {
247
247
  steeringMode: settingsManager.getSteeringMode(),
248
248
  followUpMode: settingsManager.getFollowUpMode(),
249
249
  transport: settingsManager.getTransport(),
250
- agentLoopFramework: settingsManager.getAgentLoopFramework(),
250
+ agentLoopFramework: options.agentLoopFramework ?? settingsManager.getAgentLoopFramework(),
251
251
  thinkingBudgets: settingsManager.getThinkingBudgets(),
252
252
  maxRetryDelayMs: settingsManager.getRetrySettings().maxDelayMs,
253
- maxToolResultBatchSizeChars: settingsManager.getAgentLoopSettings().maxToolResultBatchSizeChars,
253
+ maxToolResultBatchSizeChars: options.maxToolResultBatchSizeChars ??
254
+ options.loopPolicy?.maxToolResultBatchSizeChars ??
255
+ settingsManager.getAgentLoopSettings().maxToolResultBatchSizeChars,
256
+ maxToolConcurrency: options.maxToolConcurrency ?? options.loopPolicy?.maxToolConcurrency,
257
+ maxTurnsPerPrompt: options.maxTurnsPerPrompt ?? options.loopPolicy?.maxTurnsPerPrompt,
258
+ maxToolCallsPerPrompt: options.maxToolCallsPerPrompt ?? options.loopPolicy?.maxToolCallsPerPrompt,
259
+ outputTokenBudget: options.outputTokenBudget ?? options.loopPolicy?.outputTokenBudget,
260
+ maxOutputTokenRecoveryAttempts: options.maxOutputTokenRecoveryAttempts ?? options.loopPolicy?.maxOutputTokenRecoveryAttempts,
261
+ maxModelErrorRecoveryAttempts: options.maxModelErrorRecoveryAttempts ?? options.loopPolicy?.maxModelErrorRecoveryAttempts,
262
+ maxStopHookContinuations: options.maxStopHookContinuations ?? options.loopPolicy?.maxStopHookContinuations,
254
263
  getApiKey: async (provider) => {
255
264
  // Use the provider argument from the in-flight request;
256
265
  // agent.state.model may already be switched mid-turn.
@@ -68,11 +68,11 @@ loop/scheduler-controller.ts: SchedulerController - in-memory recurring task sto
68
68
  loop/scheduler-parser.ts: Loop command parsing with flags/subcommands, parseSchedulerCommand/parseDurationSpec/buildSchedulerHelp, --name/--max/--quiet
69
69
  loop/scheduler-types.ts: Scheduled loop types, LoopPayloadKind/ScheduledLoopTask/LoopStartSpec/ParsedSchedulerCommand
70
70
  loop/README.md: Loop extension documentation - recurring scheduler usage and flags
71
- sal/index.ts: SAL extension entry, enabled by default, registers flags, /sal:* commands, lifecycle hooks, terrain snapshot refresh, eval event emission, and stale-run cleanup scheduling; delegates config, context, runtime contracts, and tool_trace analytics to focused SAL modules
71
+ sal/index.ts: SAL extension entry, enabled by default, registers flags, /sal:* commands, lifecycle hooks including agent_result, terrain snapshot refresh, eval event emission, and stale-run cleanup scheduling; delegates config, context, runtime contracts, and tool_trace analytics to focused SAL modules
72
72
  sal/sal-config.ts: SAL build metadata, eval environment constants, credential loading, truthy parsing, stale-cleanup/A-B flag resolution, experiment id normalization, and sidecar directory resolution
73
73
  sal/sal-context.ts: SAL anchor system-prompt injection formatting plus A/B sidecar turn-record persistence
74
- sal/sal-runtime.ts: SAL shared BuildMeta/TurnState/SalRuntime contracts used across config, context, trace, and entry modules
75
- sal/sal-trace.ts: SAL tool path extraction, task intent inference, and bounded tool_trace payload construction
74
+ sal/sal-runtime.ts: SAL shared BuildMeta/TurnState/SalRuntime contracts used across config, context, trace, and entry modules, including per-turn loop outcome state
75
+ sal/sal-trace.ts: SAL tool path extraction, task intent inference, and bounded tool_trace payload construction with loop outcome summary
76
76
  sal/terrain.ts: TerrainSnapshot/TerrainNode/TerrainEdge model, buildTerrainIndex(), checkDipCoverage(), isSnapshotStale(), moduleIdForPath(), parses P2 AGENT.md and P3 file headers
77
77
  sal/anchors.ts: StructuralAnchor/AnchorResolution model, locateTask(), locateAction(), evidence-driven scoring with tunable SalWeights, CJK bigram tokenization
78
78
  sal/weights.ts: SalWeights interface, SAL_DEFAULT_WEIGHTS, loadSalWeights() reads sal-config.json from workspace or .memory-experiments/sal/
@@ -49,7 +49,7 @@ plan/exit-plan-mode-tool.ts: createExitPlanModeTool() - ExitPlanMode tool with p
49
49
  plan/plan-agents.ts: Explore/Plan subagent definitions with read-only tools for plan mode workflow
50
50
  plan/plan-validation.ts: validatePlan() - validates plan has required sections (Context, Approach, Files, Verification)
51
51
  plan/teammate-approval.ts: isInTeammateContext(), submitPlanToLeader(), formatPlanSubmittedMessage() - teammate plan approval integration
52
- sal/index.ts: SAL extension entry, enabled by default, registers --nosal/--sal-rebuild-terrain flags, /sal:coverage /sal:status /sal:setup commands, before_agent_start/tool_execution_start/tool_execution_end/agent_end hooks; /sal:setup writes ~/.memory-experiments/credentials.json with adapter inference (insforge/jsonl/noop); publishes structuralAnchor via core/runtime/turn-context (no SAL-specific globals); emits run_start/turn_anchor/memory_recalls/tool_trace/run_end eval events through pluggable EvalSink; reads memoryRecallSnapshot from turn-context bus in agent_end; runtime no-op when --nosal is set; auto-injects pencil_version from build-meta.json into run_start; emergency flush on beforeExit/SIGHUP/SIGTERM; stale run cleanup is opt-in via NANOPENCIL_EVAL_CLEANUP_STALE_RUNS / credentials cleanup_stale_runs; tool_trace is a bounded per-turn summary and includes no-tool turns
52
+ sal/index.ts: SAL extension entry, enabled by default, registers --nosal/--sal-rebuild-terrain flags, /sal:coverage /sal:status /sal:setup commands, before_agent_start/tool_execution_start/tool_execution_end/agent_result/agent_end hooks; /sal:setup writes ~/.memory-experiments/credentials.json with adapter inference (insforge/jsonl/noop); publishes structuralAnchor via core/runtime/turn-context (no SAL-specific globals); emits run_start/turn_anchor/memory_recalls/tool_trace/run_end eval events through pluggable EvalSink; reads memoryRecallSnapshot from turn-context bus in agent_end; runtime no-op when --nosal is set; auto-injects pencil_version from build-meta.json into run_start; emergency flush on beforeExit/SIGHUP/SIGTERM; stale run cleanup is opt-in via NANOPENCIL_EVAL_CLEANUP_STALE_RUNS / credentials cleanup_stale_runs; tool_trace is a bounded per-turn summary and includes no-tool turns plus loop outcome
53
53
  sal/terrain.ts: TerrainSnapshot/TerrainNode/TerrainEdge model, async buildTerrainIndex()/isSnapshotStale() (fs/promises + periodic yields so TUI can flush under block terminals like Warp), checkDipCoverage(), moduleIdForPath(), parses P2 CLAUDE.md and P3 file headers
54
54
  sal/anchors.ts: StructuralAnchor/AnchorResolution model, locateTask(), locateAction(), evidence-driven scoring with tunable SalWeights, CJK bigram tokenization
55
55
  sal/weights.ts: SalWeights interface, SAL_DEFAULT_WEIGHTS, loadSalWeights() reads sal-config.json from workspace or .memory-experiments/sal/
@@ -1,8 +1,8 @@
1
1
  /**
2
- * [WHO]: SAL extension entry - enabled by default, registers --nosal/--sal-ab/--sal-rebuild-terrain flags, /sal:coverage /sal:status /sal:setup commands, before_agent_start/tool_execution_start/tool_execution_end/agent_end hooks; runtime no-op when --nosal is set
2
+ * [WHO]: SAL extension entry - enabled by default, registers --nosal/--sal-ab/--sal-rebuild-terrain flags, /sal:coverage /sal:status /sal:setup commands, before_agent_start/tool_execution_start/tool_execution_end/agent_result/agent_end hooks; runtime no-op when --nosal is set
3
3
  * [FROM]: Depends on core/extensions/types.ts (ToolExecutionStartEvent, ToolExecutionEndEvent), core/runtime/turn-context.ts (publishes structuralAnchor), extensions/defaults/sal/terrain.ts, anchors.ts, weights.ts, eval/index.ts (pluggable adapters)
4
4
  * [TO]: Loaded by builtin-extensions.ts as a default extension entry point
5
- * [HERE]: extensions/defaults/sal/index.ts - pluggable Structural Anchor Localization (SAL) extension; emits run_start/turn_anchor/tool_trace/run_end eval events with best-effort flush/close isolation; tool_trace captures per-turn tool usage profile (call counts, sequences, intent, errors) for self-awareness analytics
5
+ * [HERE]: extensions/defaults/sal/index.ts - pluggable Structural Anchor Localization (SAL) extension; emits run_start/turn_anchor/tool_trace/run_end eval events with best-effort flush/close isolation; tool_trace captures per-turn tool usage and loop outcome for self-awareness analytics
6
6
  */
7
7
  import type { ExtensionAPI } from "../../../core/extensions/types.js";
8
8
  import { SAL_DEFAULT_WEIGHTS } from "./weights.js";
@@ -1,8 +1,8 @@
1
1
  /**
2
- * [WHO]: SAL extension entry - enabled by default, registers --nosal/--sal-ab/--sal-rebuild-terrain flags, /sal:coverage /sal:status /sal:setup commands, before_agent_start/tool_execution_start/tool_execution_end/agent_end hooks; runtime no-op when --nosal is set
2
+ * [WHO]: SAL extension entry - enabled by default, registers --nosal/--sal-ab/--sal-rebuild-terrain flags, /sal:coverage /sal:status /sal:setup commands, before_agent_start/tool_execution_start/tool_execution_end/agent_result/agent_end hooks; runtime no-op when --nosal is set
3
3
  * [FROM]: Depends on core/extensions/types.ts (ToolExecutionStartEvent, ToolExecutionEndEvent), core/runtime/turn-context.ts (publishes structuralAnchor), extensions/defaults/sal/terrain.ts, anchors.ts, weights.ts, eval/index.ts (pluggable adapters)
4
4
  * [TO]: Loaded by builtin-extensions.ts as a default extension entry point
5
- * [HERE]: extensions/defaults/sal/index.ts - pluggable Structural Anchor Localization (SAL) extension; emits run_start/turn_anchor/tool_trace/run_end eval events with best-effort flush/close isolation; tool_trace captures per-turn tool usage profile (call counts, sequences, intent, errors) for self-awareness analytics
5
+ * [HERE]: extensions/defaults/sal/index.ts - pluggable Structural Anchor Localization (SAL) extension; emits run_start/turn_anchor/tool_trace/run_end eval events with best-effort flush/close isolation; tool_trace captures per-turn tool usage and loop outcome for self-awareness analytics
6
6
  */
7
7
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
8
8
  import { homedir } from "node:os";
@@ -549,6 +549,20 @@ export default async function salExtension(api) {
549
549
  record.isError = event.isError;
550
550
  }
551
551
  });
552
+ api.on("agent_result", async (event, _ctx) => {
553
+ runtime.turn.agentResult = {
554
+ stopReason: event.stopReason,
555
+ turnCount: event.turnCount,
556
+ toolCallCount: event.toolCallCount,
557
+ durationMs: event.durationMs,
558
+ usage: event.usage,
559
+ permissionDenialCount: event.permissionDenialCount,
560
+ permissionDenials: event.permissionDenials,
561
+ lastTransition: event.lastTransition,
562
+ errorMessage: event.errorMessage,
563
+ errorSubtype: event.errorSubtype,
564
+ };
565
+ });
552
566
  api.on("agent_end", async (_event, _ctx) => {
553
567
  const turnDuration = Math.max(0, Date.now() - runtime.turn.startedAtMs);
554
568
  const taskRes = runtime.turn.taskResolution;
@@ -2,9 +2,10 @@
2
2
  * [WHO]: Provides BuildMeta, ToolCallRecord, TurnState, SalDiagnosticReporter, SalRuntime shared contracts for the SAL extension
3
3
  * [FROM]: Depends on eval sink types, SAL anchors/terrain/weights types for runtime state shape
4
4
  * [TO]: Consumed by extensions/defaults/sal/index.ts plus SAL config, trace, and context helpers
5
- * [HERE]: extensions/defaults/sal/sal-runtime.ts - runtime contract boundary for Structural Anchor Localization modules
5
+ * [HERE]: extensions/defaults/sal/sal-runtime.ts - runtime contract boundary for Structural Anchor Localization modules, including per-turn loop outcome state
6
6
  */
7
7
  import type { CreateEvalSinkOptions, EvalAdapterId, EvalSink, EvalVariant } from "./eval/index.js";
8
+ import type { AgentRunResult } from "@pencil-agent/agent-core";
8
9
  import type { AnchorResolution } from "./anchors.js";
9
10
  import type { TerrainSnapshot } from "./terrain.js";
10
11
  import type { SalWeights } from "./weights.js";
@@ -24,6 +25,7 @@ export interface TurnState {
24
25
  turnId: number;
25
26
  startedAtMs: number;
26
27
  taskResolution?: AnchorResolution;
28
+ agentResult?: AgentRunResult;
27
29
  touchedFiles: Set<string>;
28
30
  toolCalls: ToolCallRecord[];
29
31
  prompt?: string;
@@ -2,6 +2,6 @@
2
2
  * [WHO]: Provides BuildMeta, ToolCallRecord, TurnState, SalDiagnosticReporter, SalRuntime shared contracts for the SAL extension
3
3
  * [FROM]: Depends on eval sink types, SAL anchors/terrain/weights types for runtime state shape
4
4
  * [TO]: Consumed by extensions/defaults/sal/index.ts plus SAL config, trace, and context helpers
5
- * [HERE]: extensions/defaults/sal/sal-runtime.ts - runtime contract boundary for Structural Anchor Localization modules
5
+ * [HERE]: extensions/defaults/sal/sal-runtime.ts - runtime contract boundary for Structural Anchor Localization modules, including per-turn loop outcome state
6
6
  */
7
7
  export {};
@@ -2,7 +2,7 @@
2
2
  * [WHO]: Provides SAL tool-path extraction, task intent inference, and bounded tool_trace payload construction
3
3
  * [FROM]: Depends on node path helpers and terrain path normalization
4
4
  * [TO]: Consumed by extensions/defaults/sal/index.ts and SAL tool trace tests
5
- * [HERE]: extensions/defaults/sal/sal-trace.ts - per-turn tool analytics boundary for Structural Anchor Localization
5
+ * [HERE]: extensions/defaults/sal/sal-trace.ts - per-turn tool and loop-outcome analytics boundary for Structural Anchor Localization
6
6
  */
7
7
  import type { TurnState } from "./sal-runtime.js";
8
8
  export type TaskIntent = "fix" | "feat" | "refactor" | "explain" | "explore" | "unknown";
@@ -2,7 +2,7 @@
2
2
  * [WHO]: Provides SAL tool-path extraction, task intent inference, and bounded tool_trace payload construction
3
3
  * [FROM]: Depends on node path helpers and terrain path normalization
4
4
  * [TO]: Consumed by extensions/defaults/sal/index.ts and SAL tool trace tests
5
- * [HERE]: extensions/defaults/sal/sal-trace.ts - per-turn tool analytics boundary for Structural Anchor Localization
5
+ * [HERE]: extensions/defaults/sal/sal-trace.ts - per-turn tool and loop-outcome analytics boundary for Structural Anchor Localization
6
6
  */
7
7
  import { isAbsolute, join, relative } from "node:path";
8
8
  import { toPosixPath } from "./terrain.js";
@@ -108,7 +108,7 @@ export function buildToolTracePayload(turn, turnDuration) {
108
108
  }));
109
109
  const sequence = turn.toolCalls.slice(0, MAX_TOOL_SEQUENCE).map((tc) => tc.tool);
110
110
  const completedToolCalls = turn.toolCalls.filter((tc) => tc.endMs != null).length;
111
- return {
111
+ const payload = {
112
112
  turn_id: turn.turnId,
113
113
  tool_calls: summarizedTools,
114
114
  tool_sequence: sequence,
@@ -126,4 +126,15 @@ export function buildToolTracePayload(turn, turnDuration) {
126
126
  truncated_tool_summary: Math.max(0, toolSummary.size - summarizedTools.length),
127
127
  duration_ms: turnDuration,
128
128
  };
129
+ if (turn.agentResult) {
130
+ payload.agent_loop = {
131
+ stop_reason: turn.agentResult.stopReason,
132
+ turn_count: turn.agentResult.turnCount,
133
+ tool_call_count: turn.agentResult.toolCallCount,
134
+ duration_ms: turn.agentResult.durationMs,
135
+ permission_denial_count: turn.agentResult.permissionDenialCount ?? 0,
136
+ last_transition_reason: turn.agentResult.lastTransition?.reason,
137
+ };
138
+ }
139
+ return payload;
129
140
  }
package/dist/main.js CHANGED
@@ -515,6 +515,12 @@ function buildSessionOptions(parsed, scopedModels, sessionManager, modelRegistry
515
515
  if (parsed.thinking) {
516
516
  options.thinkingLevel = parsed.thinking;
517
517
  }
518
+ if (parsed.agentLoopFramework) {
519
+ options.agentLoopFramework = parsed.agentLoopFramework;
520
+ }
521
+ if (parsed.loopPolicy) {
522
+ options.loopPolicy = parsed.loopPolicy;
523
+ }
518
524
  // Scoped models for Ctrl+P cycling - fill in default thinking level for models without explicit level
519
525
  if (scopedModels.length > 0) {
520
526
  const defaultThinkingLevel = settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;
@@ -894,16 +900,19 @@ export async function main(args) {
894
900
  await mode.run();
895
901
  }
896
902
  else {
897
- await runPrintMode(session, {
903
+ const printResult = await runPrintMode(session, {
898
904
  mode,
899
905
  messages: parsed.messages,
900
906
  initialMessage,
901
907
  initialImages,
908
+ printLoopResult: parsed.printLoopResult,
909
+ failOnAgentError: parsed.failOnAgentError,
910
+ failOnToolDenial: parsed.failOnToolDenial,
902
911
  });
903
912
  stopThemeWatcher();
904
913
  if (process.stdout.writableLength > 0) {
905
914
  await new Promise((resolve) => process.stdout.once("drain", resolve));
906
915
  }
907
- process.exit(0);
916
+ process.exit(printResult.exitCode);
908
917
  }
909
918
  }