@pencil-agent/nano-pencil 1.14.3 → 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.
- package/dist/build-meta.json +3 -3
- package/dist/core/config/settings-manager.d.ts +8 -1
- package/dist/core/config/settings-manager.js +9 -0
- package/dist/core/extensions/runner.d.ts +70 -4
- package/dist/core/extensions/runner.js +188 -8
- package/dist/core/extensions/types.d.ts +8 -1
- package/dist/core/i18n/slash-commands.d.ts +1 -0
- package/dist/core/i18n/slash-commands.js +1 -0
- package/dist/core/i18n/slash-commands.zh.d.ts +1 -0
- package/dist/core/i18n/slash-commands.zh.js +1 -0
- package/dist/core/runtime/agent-session.d.ts +5 -0
- package/dist/core/runtime/agent-session.js +85 -27
- package/dist/core/runtime/extension-core-bindings.d.ts +3 -3
- package/dist/core/runtime/extension-core-bindings.js +73 -20
- package/dist/core/runtime/retry-coordinator.d.ts +10 -1
- package/dist/core/runtime/retry-coordinator.js +20 -4
- package/dist/core/runtime/sdk.js +2 -1
- package/dist/core/slash-commands.d.ts +3 -0
- package/dist/core/slash-commands.js +6 -2
- package/dist/core/telemetry/batching-dispatcher.d.ts +41 -0
- package/dist/core/telemetry/batching-dispatcher.js +89 -0
- package/dist/core/telemetry/build-meta.d.ts +12 -0
- package/dist/core/telemetry/build-meta.js +57 -0
- package/dist/core/telemetry/caller-context.d.ts +32 -0
- package/dist/core/telemetry/caller-context.js +19 -0
- package/dist/core/telemetry/credentials.d.ts +27 -0
- package/dist/core/telemetry/credentials.js +87 -0
- package/dist/core/telemetry/ext-events.d.ts +89 -0
- package/dist/core/telemetry/ext-events.js +189 -0
- package/dist/core/telemetry/index.d.ts +13 -0
- package/dist/core/telemetry/index.js +6 -0
- package/dist/core/telemetry/insforge-base.d.ts +37 -0
- package/dist/core/telemetry/insforge-base.js +160 -0
- package/dist/core/telemetry/types.d.ts +33 -0
- package/dist/core/telemetry/types.js +7 -0
- package/dist/extensions/defaults/browser/index.js +13 -5
- package/dist/extensions/defaults/btw/index.js +2 -2
- package/dist/extensions/defaults/debug/index.js +18 -8
- package/dist/extensions/defaults/diagnostics/index.js +12 -0
- package/dist/extensions/defaults/grub/grub-parser.d.ts +15 -1
- package/dist/extensions/defaults/grub/grub-parser.js +31 -1
- package/dist/extensions/defaults/grub/index.d.ts +1 -1
- package/dist/extensions/defaults/grub/index.js +4 -3
- package/dist/extensions/defaults/interview/index.js +2 -2
- package/dist/extensions/defaults/link-world/index.js +13 -5
- package/dist/extensions/defaults/loop/index.js +35 -0
- package/dist/extensions/defaults/mcp/index.js +18 -0
- package/dist/extensions/defaults/plan/index.js +29 -11
- package/dist/extensions/defaults/presence/index.d.ts +12 -2
- package/dist/extensions/defaults/presence/index.js +77 -23
- package/dist/extensions/defaults/presence/presence-memory.d.ts +2 -1
- package/dist/extensions/defaults/presence/presence-memory.js +37 -1
- package/dist/extensions/defaults/recap/index.js +12 -0
- package/dist/extensions/defaults/sal/eval/insforge-sink.d.ts +6 -17
- package/dist/extensions/defaults/sal/eval/insforge-sink.js +40 -183
- package/dist/extensions/defaults/sal/index.js +31 -8
- package/dist/extensions/defaults/sal/sal-config.d.ts +5 -0
- package/dist/extensions/defaults/sal/sal-config.js +15 -82
- package/dist/extensions/defaults/security-audit/index.js +138 -80
- package/dist/extensions/defaults/subagent/index.js +29 -5
- package/dist/extensions/defaults/team/index.js +10 -9
- package/dist/extensions/defaults/team/team-parser.d.ts +18 -0
- package/dist/extensions/defaults/team/team-parser.js +91 -3
- package/dist/extensions/defaults/token-save/index.js +11 -5
- package/dist/extensions/optional/export-html/index.js +19 -5
- package/dist/extensions/optional/simplify/index.js +11 -5
- package/dist/modes/interactive/interactive-mode.d.ts +2 -1
- package/dist/modes/interactive/interactive-mode.js +64 -5
- package/dist/modes/interactive/slash-command-arguments.d.ts +16 -0
- package/dist/modes/interactive/slash-command-arguments.js +97 -0
- package/dist/modes/rpc/rpc-mode.d.ts +3 -0
- package/dist/modes/rpc/rpc-mode.js +40 -31
- package/dist/modes/rpc/rpc-types.d.ts +3 -0
- package/dist/node_modules/@pencil-agent/agent-core/agent.d.ts +15 -1
- package/dist/node_modules/@pencil-agent/agent-core/agent.js +13 -1
- package/dist/node_modules/@pencil-agent/agent-core/index.d.ts +2 -1
- package/dist/node_modules/@pencil-agent/agent-core/index.js +2 -1
- package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-agent-loop.d.ts +1 -1
- package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-agent-loop.js +293 -20
- package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-streaming-tool-executor.d.ts +33 -0
- package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-streaming-tool-executor.js +189 -0
- package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-tool-orchestration.d.ts +9 -0
- package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-tool-orchestration.js +30 -5
- package/dist/node_modules/@pencil-agent/agent-core/types.d.ts +90 -3
- package/dist/node_modules/@pencil-agent/agent-core/types.js +1 -1
- package/dist/node_modules/@pencil-agent/ai/models.generated.d.ts +0 -17
- package/dist/node_modules/@pencil-agent/ai/models.generated.js +15 -32
- package/dist/node_modules/@pencil-agent/tui/autocomplete.d.ts +8 -1
- package/dist/node_modules/@pencil-agent/tui/autocomplete.js +12 -1
- package/dist/node_modules/@pencil-agent/tui/index.d.ts +1 -1
- package/dist/node_modules/@pencil-agent/tui/tui.js +5 -2
- package/dist/packages/mem-core/extension.js +6 -2
- package/dist/packages/mem-core/full-insights-sections.d.ts +48 -0
- package/dist/packages/mem-core/full-insights-sections.js +231 -0
- package/dist/packages/mem-core/full-insights.d.ts +1 -1
- package/dist/packages/mem-core/full-insights.js +102 -42
- package/package.json +2 -2
package/dist/build-meta.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* [WHO]: SettingsManager class, two-tier settings (global + project-local)
|
|
2
|
+
* [WHO]: SettingsManager class, two-tier settings (global + project-local), agent loop defaults
|
|
3
3
|
* [FROM]: Depends on ai, node:fs, proper-lockfile, config.ts
|
|
4
4
|
* [TO]: Consumed by index.ts, main.ts, core/runtime/sdk.ts, cli/config-selector.ts, extensions/defaults/team/index.ts, modes/interactive/components/model-selector.ts, modes/interactive/components/config-selector.ts, and test files
|
|
5
5
|
* [HERE]: core/config/settings-manager.ts - user preferences aggregation
|
|
@@ -20,6 +20,9 @@ export interface RetrySettings {
|
|
|
20
20
|
baseDelayMs?: number;
|
|
21
21
|
maxDelayMs?: number;
|
|
22
22
|
}
|
|
23
|
+
export interface AgentLoopSettings {
|
|
24
|
+
maxToolResultBatchSizeChars?: number;
|
|
25
|
+
}
|
|
23
26
|
export interface TerminalSettings {
|
|
24
27
|
showImages?: boolean;
|
|
25
28
|
clearOnShrink?: boolean;
|
|
@@ -65,6 +68,7 @@ export interface Settings {
|
|
|
65
68
|
defaultModel?: string;
|
|
66
69
|
defaultThinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
67
70
|
agentLoopFramework?: AgentLoopFrameworkSettingInput;
|
|
71
|
+
agentLoop?: AgentLoopSettings;
|
|
68
72
|
transport?: TransportSetting;
|
|
69
73
|
steeringMode?: "all" | "one-at-a-time";
|
|
70
74
|
followUpMode?: "all" | "one-at-a-time";
|
|
@@ -218,6 +222,9 @@ export declare class SettingsManager {
|
|
|
218
222
|
getDefaultThinkingLevel(): "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | undefined;
|
|
219
223
|
setDefaultThinkingLevel(level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"): void;
|
|
220
224
|
getAgentLoopFramework(): AgentLoopFrameworkSetting | undefined;
|
|
225
|
+
getAgentLoopSettings(): {
|
|
226
|
+
maxToolResultBatchSizeChars: number;
|
|
227
|
+
};
|
|
221
228
|
setAgentLoopFramework(framework: AgentLoopFrameworkSettingInput | undefined): void;
|
|
222
229
|
getTransport(): TransportSetting;
|
|
223
230
|
setTransport(transport: TransportSetting): void;
|
|
@@ -4,6 +4,7 @@ import lockfile from "proper-lockfile";
|
|
|
4
4
|
import { APP_NAME, CONFIG_DIR_NAME } from "../../config.js";
|
|
5
5
|
import { defaultAgentDirContext } from "../agent-dir/agent-dir-context.js";
|
|
6
6
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
7
|
+
const DEFAULT_MAX_TOOL_RESULT_BATCH_SIZE_CHARS = 200_000;
|
|
7
8
|
export function normalizeAgentLoopFrameworkSetting(value) {
|
|
8
9
|
if (value === "high-intelligence")
|
|
9
10
|
return "standard";
|
|
@@ -574,6 +575,14 @@ export class SettingsManager {
|
|
|
574
575
|
getAgentLoopFramework() {
|
|
575
576
|
return normalizeAgentLoopFrameworkSetting(this.settings.agentLoopFramework);
|
|
576
577
|
}
|
|
578
|
+
getAgentLoopSettings() {
|
|
579
|
+
const configured = this.settings.agentLoop?.maxToolResultBatchSizeChars;
|
|
580
|
+
return {
|
|
581
|
+
maxToolResultBatchSizeChars: typeof configured === "number" && Number.isFinite(configured) && configured > 0
|
|
582
|
+
? Math.floor(configured)
|
|
583
|
+
: DEFAULT_MAX_TOOL_RESULT_BATCH_SIZE_CHARS,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
577
586
|
setAgentLoopFramework(framework) {
|
|
578
587
|
const normalized = normalizeAgentLoopFrameworkSetting(framework);
|
|
579
588
|
if (normalized === undefined) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* [WHO]: ExtensionRunner class, lifecycle management, event emission
|
|
3
|
-
* [FROM]: Depends on agent-core, ai, tui, modes/theme, session-manager, types.ts
|
|
4
|
-
* [TO]: Consumed by core/extensions/index.ts, core/extensions/wrapper.ts
|
|
5
|
-
* [HERE]: core/extensions/runner.ts - extension execution and lifecycle management
|
|
2
|
+
* [WHO]: ExtensionRunner class, lifecycle management, event emission, slash-command dispatch chokepoint (invokeCommand), telemetry sink wiring (setTelemetrySink)
|
|
3
|
+
* [FROM]: Depends on agent-core, ai, tui, modes/theme, session-manager, types.ts, core/telemetry (ExtensionTelemetrySink + classifyArgsSignature for the P1 ext_command_events writer)
|
|
4
|
+
* [TO]: Consumed by core/extensions/index.ts, core/extensions/wrapper.ts, core/runtime/agent-session.ts (delegates command dispatch via invokeCommand)
|
|
5
|
+
* [HERE]: core/extensions/runner.ts - extension execution and lifecycle management; owns the single try/catch around command.handler so telemetry can wrap every invocation regardless of caller mode
|
|
6
6
|
*/
|
|
7
7
|
import type { AgentMessage } from "@pencil-agent/agent-core";
|
|
8
8
|
import type { ImageContent } from "@pencil-agent/ai";
|
|
@@ -11,6 +11,7 @@ import type { ResourceDiagnostic } from "../diagnostics.js";
|
|
|
11
11
|
import type { KeybindingsConfig } from "../keybindings.js";
|
|
12
12
|
import type { ModelRegistry } from "../model-registry.js";
|
|
13
13
|
import type { SessionManager } from "../session/session-manager.js";
|
|
14
|
+
import { type ExtensionTelemetrySink, type LlmCallEventInput } from "../telemetry/index.js";
|
|
14
15
|
import type { BeforeAgentStartEvent, BeforeAgentStartEventResult, ContextEvent, Extension, ExtensionActions, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFlag, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, InputEvent, InputEventResult, InputSource, MessageRenderer, RegisteredCommand, RegisteredTool, ResourcesDiscoverEvent, SessionBeforeCompactResult, SessionBeforeForkResult, SessionBeforeSwitchResult, SessionBeforeTreeResult, ToolCallEvent, ToolCallEventResult, ToolResultEvent, ToolResultEventResult, UserBashEvent, UserBashEventResult } from "./types.js";
|
|
15
16
|
/** Combined result from all before_agent_start handlers */
|
|
16
17
|
interface BeforeAgentStartCombinedResult {
|
|
@@ -90,6 +91,7 @@ export declare class ExtensionRunner {
|
|
|
90
91
|
private shutdownHandler;
|
|
91
92
|
private shortcutDiagnostics;
|
|
92
93
|
private commandDiagnostics;
|
|
94
|
+
private telemetrySink?;
|
|
93
95
|
private _beforeAgentStartTimeoutMs;
|
|
94
96
|
private get beforeAgentStartTimeoutMs();
|
|
95
97
|
private readonly beforeAgentStartTimeoutSentinel;
|
|
@@ -127,6 +129,70 @@ export declare class ExtensionRunner {
|
|
|
127
129
|
extensionPath: string;
|
|
128
130
|
}>;
|
|
129
131
|
getCommand(name: string): RegisteredCommand | undefined;
|
|
132
|
+
/**
|
|
133
|
+
* Returns the owning Extension for a given slash command name. Used by the
|
|
134
|
+
* telemetry middleware to stamp `extension_name` on each ext_command_events
|
|
135
|
+
* row. Falls back to undefined when no extension claims the command.
|
|
136
|
+
*/
|
|
137
|
+
private findCommandOwner;
|
|
138
|
+
/**
|
|
139
|
+
* Derive a short, stable extension name from an Extension record. For
|
|
140
|
+
* built-ins (extensions/defaults/<name>) and most user extensions
|
|
141
|
+
* (packages/<name>) the directory basename is the right answer.
|
|
142
|
+
*/
|
|
143
|
+
private deriveExtensionName;
|
|
144
|
+
/**
|
|
145
|
+
* Attach (or replace) the extension telemetry sink. The runner owns sink
|
|
146
|
+
* lifecycle from this point — invokeCommand() will fire-and-forget one
|
|
147
|
+
* `ext_command_events` row per invocation, and writeLlmCallEvent() (called
|
|
148
|
+
* from extension-core-bindings) writes one `ext_llm_calls` row per
|
|
149
|
+
* extension-initiated LLM call. Passing the noop sink (the factory's
|
|
150
|
+
* default when no insforge credentials exist) is the safe way to disable
|
|
151
|
+
* telemetry without scattering null-checks at the call sites.
|
|
152
|
+
*/
|
|
153
|
+
setTelemetrySink(sink: ExtensionTelemetrySink): void;
|
|
154
|
+
/**
|
|
155
|
+
* Passthrough used by core/runtime/extension-core-bindings.ts after each
|
|
156
|
+
* extension-initiated LLM call. The runner owns the sink; the binding
|
|
157
|
+
* doesn't import it directly to keep its concerns scoped to LLM plumbing.
|
|
158
|
+
*/
|
|
159
|
+
writeLlmCallEvent(input: LlmCallEventInput): void;
|
|
160
|
+
/**
|
|
161
|
+
* Wrap an extension hook handler invocation in three layers:
|
|
162
|
+
*
|
|
163
|
+
* 1. AsyncLocalStorage frame so any LLM call placed by the handler gets
|
|
164
|
+
* attributed to this extension + hook + isUserInitiated=false in
|
|
165
|
+
* ext_llm_calls.
|
|
166
|
+
* 2. Wall-clock timing measurement (only when the sample roll passes).
|
|
167
|
+
* 3. One fire-and-forget ext_hook_events row per sampled invocation,
|
|
168
|
+
* capturing duration_ms + ok + error_code + sample_rate.
|
|
169
|
+
*
|
|
170
|
+
* High-frequency hooks (tool_*) are sampled at 10% so the table doesn't
|
|
171
|
+
* drown in tool-execution rows; the sample_rate column on each row lets
|
|
172
|
+
* dashboards extrapolate with `count(*) * 1/sample_rate`. Sampling decision
|
|
173
|
+
* sits inside the AsyncLocalStorage frame so even skipped-emit hooks still
|
|
174
|
+
* attribute their LLM calls.
|
|
175
|
+
*/
|
|
176
|
+
private invokeHookHandler;
|
|
177
|
+
/**
|
|
178
|
+
* Single chokepoint for slash command dispatch. All modes (interactive /
|
|
179
|
+
* print / rpc / acp) funnel through agent-session._tryExecuteExtensionCommand,
|
|
180
|
+
* which calls this method. The wrapper measures wall-clock duration,
|
|
181
|
+
* captures outcome (ok / error / cancelled), and emits one telemetry row
|
|
182
|
+
* per invocation. Errors are still routed through emitError() so existing
|
|
183
|
+
* UI surfaces (toasts, logs) keep working unchanged.
|
|
184
|
+
*
|
|
185
|
+
* Returns `{ found: false }` when no extension owns the command, letting
|
|
186
|
+
* the caller fall through to built-in command handling without emitting a
|
|
187
|
+
* telemetry row for an unknown command.
|
|
188
|
+
*/
|
|
189
|
+
invokeCommand(commandName: string, args: string, ctx: ExtensionCommandContext, metadata?: {
|
|
190
|
+
sessionId?: string | null;
|
|
191
|
+
runId?: string | null;
|
|
192
|
+
variant?: string | null;
|
|
193
|
+
}): Promise<{
|
|
194
|
+
found: boolean;
|
|
195
|
+
}>;
|
|
130
196
|
/**
|
|
131
197
|
* Request a graceful shutdown. Called by extension tools and event handlers.
|
|
132
198
|
* The actual shutdown behavior is provided by the mode via bindExtensions().
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { theme } from "../../modes/interactive/theme/theme.js";
|
|
2
|
+
import { classifyArgsSignature, HOOK_SAMPLE_RATES, runWithExtCallerContext, } from "../telemetry/index.js";
|
|
2
3
|
// Keybindings for these actions cannot be overridden by extensions
|
|
3
4
|
const RESERVED_ACTIONS_FOR_EXTENSION_CONFLICTS = [
|
|
4
5
|
"interrupt",
|
|
@@ -108,6 +109,7 @@ export class ExtensionRunner {
|
|
|
108
109
|
shutdownHandler = () => { };
|
|
109
110
|
shortcutDiagnostics = [];
|
|
110
111
|
commandDiagnostics = [];
|
|
112
|
+
telemetrySink;
|
|
111
113
|
_beforeAgentStartTimeoutMs = 1500;
|
|
112
114
|
get beforeAgentStartTimeoutMs() { return this._beforeAgentStartTimeoutMs ?? 1500; }
|
|
113
115
|
beforeAgentStartTimeoutSentinel = Symbol("before_agent_start_timeout");
|
|
@@ -378,6 +380,184 @@ export class ExtensionRunner {
|
|
|
378
380
|
}
|
|
379
381
|
return undefined;
|
|
380
382
|
}
|
|
383
|
+
/**
|
|
384
|
+
* Returns the owning Extension for a given slash command name. Used by the
|
|
385
|
+
* telemetry middleware to stamp `extension_name` on each ext_command_events
|
|
386
|
+
* row. Falls back to undefined when no extension claims the command.
|
|
387
|
+
*/
|
|
388
|
+
findCommandOwner(name) {
|
|
389
|
+
for (const ext of this.extensions) {
|
|
390
|
+
if (ext.commands.has(name))
|
|
391
|
+
return ext;
|
|
392
|
+
}
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Derive a short, stable extension name from an Extension record. For
|
|
397
|
+
* built-ins (extensions/defaults/<name>) and most user extensions
|
|
398
|
+
* (packages/<name>) the directory basename is the right answer.
|
|
399
|
+
*/
|
|
400
|
+
deriveExtensionName(ext) {
|
|
401
|
+
const path = ext.path || ext.resolvedPath || "";
|
|
402
|
+
const segments = path.replace(/\/+$/, "").split(/[\\/]/);
|
|
403
|
+
return segments[segments.length - 1] || "unknown";
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Attach (or replace) the extension telemetry sink. The runner owns sink
|
|
407
|
+
* lifecycle from this point — invokeCommand() will fire-and-forget one
|
|
408
|
+
* `ext_command_events` row per invocation, and writeLlmCallEvent() (called
|
|
409
|
+
* from extension-core-bindings) writes one `ext_llm_calls` row per
|
|
410
|
+
* extension-initiated LLM call. Passing the noop sink (the factory's
|
|
411
|
+
* default when no insforge credentials exist) is the safe way to disable
|
|
412
|
+
* telemetry without scattering null-checks at the call sites.
|
|
413
|
+
*/
|
|
414
|
+
setTelemetrySink(sink) {
|
|
415
|
+
this.telemetrySink = sink;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Passthrough used by core/runtime/extension-core-bindings.ts after each
|
|
419
|
+
* extension-initiated LLM call. The runner owns the sink; the binding
|
|
420
|
+
* doesn't import it directly to keep its concerns scoped to LLM plumbing.
|
|
421
|
+
*/
|
|
422
|
+
writeLlmCallEvent(input) {
|
|
423
|
+
try {
|
|
424
|
+
this.telemetrySink?.writeLlmCallEvent(input);
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
// Telemetry must never destabilize the LLM call path.
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Wrap an extension hook handler invocation in three layers:
|
|
432
|
+
*
|
|
433
|
+
* 1. AsyncLocalStorage frame so any LLM call placed by the handler gets
|
|
434
|
+
* attributed to this extension + hook + isUserInitiated=false in
|
|
435
|
+
* ext_llm_calls.
|
|
436
|
+
* 2. Wall-clock timing measurement (only when the sample roll passes).
|
|
437
|
+
* 3. One fire-and-forget ext_hook_events row per sampled invocation,
|
|
438
|
+
* capturing duration_ms + ok + error_code + sample_rate.
|
|
439
|
+
*
|
|
440
|
+
* High-frequency hooks (tool_*) are sampled at 10% so the table doesn't
|
|
441
|
+
* drown in tool-execution rows; the sample_rate column on each row lets
|
|
442
|
+
* dashboards extrapolate with `count(*) * 1/sample_rate`. Sampling decision
|
|
443
|
+
* sits inside the AsyncLocalStorage frame so even skipped-emit hooks still
|
|
444
|
+
* attribute their LLM calls.
|
|
445
|
+
*/
|
|
446
|
+
invokeHookHandler(ext, hookName, fn) {
|
|
447
|
+
const extensionName = this.deriveExtensionName(ext);
|
|
448
|
+
const ctx = {
|
|
449
|
+
extensionName,
|
|
450
|
+
callerContext: `hook:${hookName}`,
|
|
451
|
+
isUserInitiated: false,
|
|
452
|
+
};
|
|
453
|
+
return runWithExtCallerContext(ctx, async () => {
|
|
454
|
+
const sampleRate = HOOK_SAMPLE_RATES[hookName] ?? 1.0;
|
|
455
|
+
if (sampleRate < 1.0 && Math.random() >= sampleRate) {
|
|
456
|
+
return await fn();
|
|
457
|
+
}
|
|
458
|
+
const recordedAt = new Date();
|
|
459
|
+
const startPerf = performance.now();
|
|
460
|
+
let ok = true;
|
|
461
|
+
let errorCode = null;
|
|
462
|
+
try {
|
|
463
|
+
return await fn();
|
|
464
|
+
}
|
|
465
|
+
catch (err) {
|
|
466
|
+
ok = false;
|
|
467
|
+
errorCode = err instanceof Error ? err.constructor.name : "unknown";
|
|
468
|
+
throw err;
|
|
469
|
+
}
|
|
470
|
+
finally {
|
|
471
|
+
try {
|
|
472
|
+
this.telemetrySink?.writeHookEvent({
|
|
473
|
+
extensionName,
|
|
474
|
+
hookName,
|
|
475
|
+
durationMs: Math.round(performance.now() - startPerf),
|
|
476
|
+
ok,
|
|
477
|
+
errorCode,
|
|
478
|
+
sampleRate,
|
|
479
|
+
recordedAt,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
// Telemetry never destabilizes hook dispatch.
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Single chokepoint for slash command dispatch. All modes (interactive /
|
|
490
|
+
* print / rpc / acp) funnel through agent-session._tryExecuteExtensionCommand,
|
|
491
|
+
* which calls this method. The wrapper measures wall-clock duration,
|
|
492
|
+
* captures outcome (ok / error / cancelled), and emits one telemetry row
|
|
493
|
+
* per invocation. Errors are still routed through emitError() so existing
|
|
494
|
+
* UI surfaces (toasts, logs) keep working unchanged.
|
|
495
|
+
*
|
|
496
|
+
* Returns `{ found: false }` when no extension owns the command, letting
|
|
497
|
+
* the caller fall through to built-in command handling without emitting a
|
|
498
|
+
* telemetry row for an unknown command.
|
|
499
|
+
*/
|
|
500
|
+
async invokeCommand(commandName, args, ctx, metadata) {
|
|
501
|
+
const command = this.getCommand(commandName);
|
|
502
|
+
if (!command)
|
|
503
|
+
return { found: false };
|
|
504
|
+
const ownerExt = this.findCommandOwner(commandName);
|
|
505
|
+
const extensionName = ownerExt ? this.deriveExtensionName(ownerExt) : "unknown";
|
|
506
|
+
const startedAt = new Date();
|
|
507
|
+
const startPerf = performance.now();
|
|
508
|
+
let outcome = "ok";
|
|
509
|
+
let errorCode = null;
|
|
510
|
+
let thrown;
|
|
511
|
+
const argsSig = classifyArgsSignature(args);
|
|
512
|
+
const callerCtx = {
|
|
513
|
+
extensionName,
|
|
514
|
+
callerContext: argsSig === "no-args" ? `command:/${commandName}` : `command:/${commandName} ${argsSig}`,
|
|
515
|
+
isUserInitiated: true,
|
|
516
|
+
sessionId: metadata?.sessionId ?? null,
|
|
517
|
+
runId: metadata?.runId ?? null,
|
|
518
|
+
variant: metadata?.variant ?? null,
|
|
519
|
+
};
|
|
520
|
+
try {
|
|
521
|
+
await runWithExtCallerContext(callerCtx, () => command.handler(args, ctx));
|
|
522
|
+
}
|
|
523
|
+
catch (err) {
|
|
524
|
+
outcome = "error";
|
|
525
|
+
errorCode = err instanceof Error ? err.constructor.name : "unknown";
|
|
526
|
+
thrown = err;
|
|
527
|
+
this.emitError({
|
|
528
|
+
extensionPath: `command:${commandName}`,
|
|
529
|
+
event: "command",
|
|
530
|
+
error: err instanceof Error ? err.message : String(err),
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
const durationMs = Math.round(performance.now() - startPerf);
|
|
534
|
+
const endedAt = new Date();
|
|
535
|
+
try {
|
|
536
|
+
this.telemetrySink?.writeCommandEvent({
|
|
537
|
+
extensionName,
|
|
538
|
+
commandName,
|
|
539
|
+
argsSignature: argsSig,
|
|
540
|
+
argsLength: args.length,
|
|
541
|
+
outcome,
|
|
542
|
+
errorCode,
|
|
543
|
+
durationMs,
|
|
544
|
+
startedAt,
|
|
545
|
+
endedAt,
|
|
546
|
+
sessionId: metadata?.sessionId ?? null,
|
|
547
|
+
runId: metadata?.runId ?? null,
|
|
548
|
+
variant: metadata?.variant ?? null,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
// Sink emission must never bring down command dispatch.
|
|
553
|
+
}
|
|
554
|
+
// Even though we logged the error to telemetry + emitError, callers
|
|
555
|
+
// expecting the thrown error (e.g. AgentSession previously re-caught it
|
|
556
|
+
// silently) won't see it. This matches the original behaviour: the old
|
|
557
|
+
// _tryExecuteExtensionCommand swallowed the error after emitError too.
|
|
558
|
+
void thrown;
|
|
559
|
+
return { found: true };
|
|
560
|
+
}
|
|
381
561
|
/**
|
|
382
562
|
* Request a graceful shutdown. Called by extension tools and event handlers.
|
|
383
563
|
* The actual shutdown behavior is provided by the mode via bindExtensions().
|
|
@@ -444,7 +624,7 @@ export class ExtensionRunner {
|
|
|
444
624
|
continue;
|
|
445
625
|
for (const handler of handlers) {
|
|
446
626
|
try {
|
|
447
|
-
const handlerResult = await handler(event, ctx);
|
|
627
|
+
const handlerResult = await this.invokeHookHandler(ext, event.type, () => handler(event, ctx));
|
|
448
628
|
if (this.isSessionBeforeEvent(event) && handlerResult) {
|
|
449
629
|
result = handlerResult;
|
|
450
630
|
if (result.cancel) {
|
|
@@ -476,7 +656,7 @@ export class ExtensionRunner {
|
|
|
476
656
|
continue;
|
|
477
657
|
for (const handler of handlers) {
|
|
478
658
|
try {
|
|
479
|
-
const handlerResult = (await handler(currentEvent, ctx));
|
|
659
|
+
const handlerResult = (await this.invokeHookHandler(ext, "tool_result", () => handler(currentEvent, ctx)));
|
|
480
660
|
if (!handlerResult)
|
|
481
661
|
continue;
|
|
482
662
|
if (handlerResult.content !== undefined) {
|
|
@@ -521,7 +701,7 @@ export class ExtensionRunner {
|
|
|
521
701
|
if (!handlers || handlers.length === 0)
|
|
522
702
|
continue;
|
|
523
703
|
for (const handler of handlers) {
|
|
524
|
-
const handlerResult = await handler(event, ctx);
|
|
704
|
+
const handlerResult = await this.invokeHookHandler(ext, "tool_call", () => handler(event, ctx));
|
|
525
705
|
if (handlerResult) {
|
|
526
706
|
result = handlerResult;
|
|
527
707
|
if (result.block) {
|
|
@@ -540,7 +720,7 @@ export class ExtensionRunner {
|
|
|
540
720
|
continue;
|
|
541
721
|
for (const handler of handlers) {
|
|
542
722
|
try {
|
|
543
|
-
const handlerResult = await handler(event, ctx);
|
|
723
|
+
const handlerResult = await this.invokeHookHandler(ext, "user_bash", () => handler(event, ctx));
|
|
544
724
|
if (handlerResult) {
|
|
545
725
|
return handlerResult;
|
|
546
726
|
}
|
|
@@ -569,7 +749,7 @@ export class ExtensionRunner {
|
|
|
569
749
|
for (const handler of handlers) {
|
|
570
750
|
try {
|
|
571
751
|
const event = { type: "context", messages: currentMessages };
|
|
572
|
-
const handlerResult = await handler(event, ctx);
|
|
752
|
+
const handlerResult = await this.invokeHookHandler(ext, "context", () => handler(event, ctx));
|
|
573
753
|
if (handlerResult && handlerResult.messages) {
|
|
574
754
|
currentMessages = handlerResult.messages;
|
|
575
755
|
}
|
|
@@ -605,7 +785,7 @@ export class ExtensionRunner {
|
|
|
605
785
|
images,
|
|
606
786
|
systemPrompt: currentSystemPrompt,
|
|
607
787
|
};
|
|
608
|
-
const handlerResult = await this.withTimeout(handler(event, ctx), this.beforeAgentStartTimeoutMs);
|
|
788
|
+
const handlerResult = await this.withTimeout(this.invokeHookHandler(ext, "before_agent_start", () => handler(event, ctx)), this.beforeAgentStartTimeoutMs);
|
|
609
789
|
if (handlerResult === this.beforeAgentStartTimeoutSentinel) {
|
|
610
790
|
this.reportBeforeAgentStartTimeout(ext.path);
|
|
611
791
|
continue;
|
|
@@ -663,7 +843,7 @@ export class ExtensionRunner {
|
|
|
663
843
|
for (const handler of handlers) {
|
|
664
844
|
try {
|
|
665
845
|
const event = { type: "resources_discover", cwd, reason };
|
|
666
|
-
const handlerResult = await handler(event, ctx);
|
|
846
|
+
const handlerResult = await this.invokeHookHandler(ext, "resources_discover", () => handler(event, ctx));
|
|
667
847
|
const result = handlerResult;
|
|
668
848
|
if (result?.skillPaths?.length) {
|
|
669
849
|
skillPaths.push(...result.skillPaths.map((path) => ({ path, extensionPath: ext.path })));
|
|
@@ -698,7 +878,7 @@ export class ExtensionRunner {
|
|
|
698
878
|
for (const handler of ext.handlers.get("input") ?? []) {
|
|
699
879
|
try {
|
|
700
880
|
const event = { type: "input", text: currentText, images: currentImages, source };
|
|
701
|
-
const result = (await handler(event, ctx));
|
|
881
|
+
const result = (await this.invokeHookHandler(ext, "input", () => handler(event, ctx)));
|
|
702
882
|
if (result?.action === "handled")
|
|
703
883
|
return result;
|
|
704
884
|
if (result?.action === "transform") {
|
|
@@ -685,10 +685,17 @@ export interface MessageRenderOptions {
|
|
|
685
685
|
expanded: boolean;
|
|
686
686
|
}
|
|
687
687
|
export type MessageRenderer<T = unknown> = (message: CustomMessage<T>, options: MessageRenderOptions, theme: Theme) => Component | undefined;
|
|
688
|
+
export interface ArgumentCompletionContext {
|
|
689
|
+
commandName: string;
|
|
690
|
+
argumentText: string;
|
|
691
|
+
argumentPrefix: string;
|
|
692
|
+
tokenIndex: number;
|
|
693
|
+
previousTokens: string[];
|
|
694
|
+
}
|
|
688
695
|
export interface RegisteredCommand {
|
|
689
696
|
name: string;
|
|
690
697
|
description?: string;
|
|
691
|
-
getArgumentCompletions?: (argumentPrefix: string) => AutocompleteItem[] | null;
|
|
698
|
+
getArgumentCompletions?: (argumentPrefix: string, context?: ArgumentCompletionContext) => AutocompleteItem[] | null;
|
|
692
699
|
handler: (args: string, ctx: ExtensionCommandContext) => Promise<void>;
|
|
693
700
|
}
|
|
694
701
|
/** Handler function type for events */
|
|
@@ -17,6 +17,7 @@ export const slashCommands = {
|
|
|
17
17
|
},
|
|
18
18
|
settings: "Open settings menu",
|
|
19
19
|
model: "Select model (opens selector UI)",
|
|
20
|
+
thinking: "Choose reasoning depth for the current model",
|
|
20
21
|
"agent-loop": "Choose how the agent keeps working through a task",
|
|
21
22
|
"scoped-models": "Choose which models appear in quick switching",
|
|
22
23
|
apikey: "Update API key for current provider",
|
|
@@ -342,6 +342,10 @@ export declare class AgentSession {
|
|
|
342
342
|
prompt(text: string, options?: PromptOptions): Promise<void>;
|
|
343
343
|
/**
|
|
344
344
|
* Try to execute an extension command. Returns true if command was found and executed.
|
|
345
|
+
*
|
|
346
|
+
* Delegates to ExtensionRunner.invokeCommand() so command dispatch, error
|
|
347
|
+
* routing (emitError), and telemetry (ext_command_events) all happen in one
|
|
348
|
+
* place rather than being scattered across modes.
|
|
345
349
|
*/
|
|
346
350
|
private _tryExecuteExtensionCommand;
|
|
347
351
|
/**
|
|
@@ -516,6 +520,7 @@ export declare class AgentSession {
|
|
|
516
520
|
* @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
|
|
517
521
|
*/
|
|
518
522
|
private _checkCompaction;
|
|
523
|
+
private _recoverModelErrorInLoop;
|
|
519
524
|
/**
|
|
520
525
|
* Internal: Run auto-compaction with events.
|
|
521
526
|
*/
|