@aliou/pi-guardrails 0.11.2 → 0.12.0

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 (94) hide show
  1. package/README.md +72 -167
  2. package/extensions/guardrails/commands/examples/index.ts +520 -0
  3. package/extensions/guardrails/commands/onboarding/config.ts +54 -0
  4. package/{src/commands/onboarding-command.ts → extensions/guardrails/commands/onboarding/index.ts} +5 -31
  5. package/extensions/guardrails/commands/settings/add-rule-wizard.ts +267 -0
  6. package/extensions/guardrails/commands/settings/examples.ts +399 -0
  7. package/extensions/guardrails/commands/settings/index.ts +596 -0
  8. package/extensions/guardrails/commands/settings/path-list-editor.ts +158 -0
  9. package/extensions/guardrails/commands/settings/scope-picker-submenu.ts +69 -0
  10. package/extensions/guardrails/commands/settings/utils.ts +108 -0
  11. package/extensions/guardrails/components/onboarding-choice-step.ts +140 -0
  12. package/extensions/guardrails/components/onboarding-finish-step.ts +50 -0
  13. package/extensions/guardrails/components/onboarding-intro-step.ts +30 -0
  14. package/extensions/guardrails/components/onboarding-types.ts +10 -0
  15. package/extensions/guardrails/components/onboarding-wizard.ts +116 -0
  16. package/{src → extensions/guardrails}/components/pattern-editor.ts +11 -10
  17. package/extensions/guardrails/index.ts +106 -0
  18. package/extensions/guardrails/rules.test.ts +107 -0
  19. package/extensions/guardrails/rules.ts +119 -0
  20. package/extensions/guardrails/targets.test.ts +44 -0
  21. package/extensions/guardrails/targets.ts +66 -0
  22. package/extensions/path-access/grants.test.ts +47 -0
  23. package/extensions/path-access/grants.ts +68 -0
  24. package/extensions/path-access/index.ts +143 -0
  25. package/extensions/path-access/prompt.ts +196 -0
  26. package/extensions/path-access/rules.test.ts +46 -0
  27. package/extensions/path-access/rules.ts +37 -0
  28. package/extensions/path-access/targets.test.ts +40 -0
  29. package/extensions/path-access/targets.ts +19 -0
  30. package/extensions/permission-gate/grants.ts +21 -0
  31. package/extensions/permission-gate/index.ts +122 -0
  32. package/extensions/permission-gate/prompt.ts +222 -0
  33. package/extensions/permission-gate/rules.test.ts +132 -0
  34. package/extensions/permission-gate/rules.ts +72 -0
  35. package/package.json +18 -20
  36. package/schema.json +286 -0
  37. package/src/core/check.test.ts +169 -0
  38. package/src/core/check.ts +38 -0
  39. package/src/{hooks/permission-gate/dangerous-commands.test.ts → core/commands/dangerous.test.ts} +134 -2
  40. package/src/{hooks/permission-gate/dangerous-commands.ts → core/commands/dangerous.ts} +119 -1
  41. package/src/core/commands/index.ts +15 -0
  42. package/src/core/index.ts +13 -0
  43. package/src/{utils/path-access.test.ts → core/paths/access.test.ts} +1 -5
  44. package/src/core/paths/index.ts +14 -0
  45. package/src/{utils → core/shell}/command-args.test.ts +31 -20
  46. package/src/core/shell/index.ts +2 -0
  47. package/src/core/types.ts +55 -0
  48. package/src/shared/config/defaults.ts +118 -0
  49. package/src/shared/config/index.ts +17 -0
  50. package/src/shared/config/loader.ts +64 -0
  51. package/src/shared/config/migration/001-v0-format-upgrade.ts +107 -0
  52. package/src/shared/config/migration/002-strip-toolchain-fields.ts +39 -0
  53. package/src/shared/config/migration/003-strip-command-explainer-fields.ts +42 -0
  54. package/src/shared/config/migration/004-env-files-to-policies.ts +87 -0
  55. package/src/shared/config/migration/005-normalize-allowed-paths.ts +43 -0
  56. package/src/shared/config/migration/006-apply-builtin-defaults.ts +19 -0
  57. package/src/shared/config/migration/007-mark-onboarding-done.ts +25 -0
  58. package/src/shared/config/migration/index.ts +44 -0
  59. package/src/shared/config/migration/version.ts +7 -0
  60. package/src/shared/config/types.ts +141 -0
  61. package/src/shared/events.ts +100 -0
  62. package/src/shared/index.ts +6 -0
  63. package/src/shared/matching.test.ts +86 -0
  64. package/src/{utils → shared}/matching.ts +4 -4
  65. package/src/{utils → shared/paths}/bash-paths.test.ts +11 -2
  66. package/src/{utils → shared/paths}/bash-paths.ts +4 -4
  67. package/src/shared/paths/index.ts +1 -0
  68. package/src/shared/warnings.ts +17 -0
  69. package/docs/defaults.md +0 -140
  70. package/docs/examples.md +0 -170
  71. package/src/commands/onboarding.ts +0 -390
  72. package/src/commands/settings-command.ts +0 -1616
  73. package/src/config.ts +0 -392
  74. package/src/hooks/index.ts +0 -11
  75. package/src/hooks/path-access.ts +0 -395
  76. package/src/hooks/permission-gate/index.test.ts +0 -332
  77. package/src/hooks/permission-gate/index.ts +0 -595
  78. package/src/hooks/policies.ts +0 -322
  79. package/src/index.ts +0 -96
  80. package/src/lib/executor.ts +0 -280
  81. package/src/lib/index.ts +0 -16
  82. package/src/lib/model-resolver.ts +0 -47
  83. package/src/lib/timing.ts +0 -42
  84. package/src/lib/types.ts +0 -115
  85. package/src/utils/events.ts +0 -32
  86. package/src/utils/migration.test.ts +0 -58
  87. package/src/utils/migration.ts +0 -340
  88. package/src/utils/warnings.ts +0 -7
  89. /package/src/{utils/path-access.ts → core/paths/access.ts} +0 -0
  90. /package/src/{utils → core/paths}/path.test.ts +0 -0
  91. /package/src/{utils → core/paths}/path.ts +0 -0
  92. /package/src/{utils/shell-utils.ts → core/shell/ast.ts} +0 -0
  93. /package/src/{utils → core/shell}/command-args.ts +0 -0
  94. /package/src/{utils/glob-expander.ts → shared/glob.ts} +0 -0
@@ -1,322 +0,0 @@
1
- import { stat } from "node:fs/promises";
2
- import { isAbsolute, relative, resolve } from "node:path";
3
- import { parse } from "@aliou/sh";
4
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
5
- import type { PolicyRule, Protection, ResolvedConfig } from "../config";
6
- import { emitBlocked } from "../utils/events";
7
- import { expandGlob, hasGlobChars } from "../utils/glob-expander";
8
- import {
9
- type CompiledPattern,
10
- compileFilePatterns,
11
- normalizeFilePath,
12
- } from "../utils/matching";
13
- import { expandHomePath, maybePathLike } from "../utils/path";
14
- import { walkCommands, wordToString } from "../utils/shell-utils";
15
- import { pendingWarnings } from "../utils/warnings";
16
-
17
- const DEFAULT_BLOCK_MESSAGES: Record<Protection, string> = {
18
- noAccess:
19
- "Accessing {file} is not allowed. This file is protected. Ask the user if changes are needed.",
20
- readOnly:
21
- "Writing to {file} is not allowed. This file is read-only. Use the read tool to inspect it instead of bash commands like cat or ls.",
22
- none: "",
23
- };
24
-
25
- const BLOCKED_TOOLS: Record<Protection, Set<string>> = {
26
- noAccess: new Set(["read", "write", "edit", "bash", "grep", "find", "ls"]),
27
- readOnly: new Set(["write", "edit", "bash"]),
28
- none: new Set(),
29
- };
30
-
31
- interface CompiledRule {
32
- id: string;
33
- protection: Protection;
34
- patterns: CompiledPattern[];
35
- allowedPatterns: CompiledPattern[];
36
- onlyIfExists: boolean;
37
- blockMessage: string;
38
- enabled: boolean;
39
- }
40
-
41
- async function fileExists(filePath: string, cwd: string): Promise<boolean> {
42
- try {
43
- await stat(resolvePolicyPath(filePath, cwd));
44
- return true;
45
- } catch {
46
- return false;
47
- }
48
- }
49
-
50
- function protectionRank(protection: Protection): number {
51
- switch (protection) {
52
- case "none":
53
- return 0;
54
- case "readOnly":
55
- return 1;
56
- case "noAccess":
57
- return 2;
58
- }
59
- }
60
-
61
- function compileRules(rules: PolicyRule[]): CompiledRule[] {
62
- const compiled: CompiledRule[] = [];
63
-
64
- for (const rule of rules) {
65
- const id = rule.id?.trim();
66
- if (!id) {
67
- pendingWarnings.push("[guardrails] skipping policy rule without id.");
68
- continue;
69
- }
70
-
71
- if (
72
- rule.protection !== "none" &&
73
- rule.protection !== "readOnly" &&
74
- rule.protection !== "noAccess"
75
- ) {
76
- pendingWarnings.push(
77
- `[guardrails] skipping policy rule "${id}": invalid protection.`,
78
- );
79
- continue;
80
- }
81
-
82
- const normalizedPatterns = (rule.patterns ?? []).filter(
83
- (pattern) => pattern.pattern.trim().length > 0,
84
- );
85
- if (normalizedPatterns.length === 0) {
86
- pendingWarnings.push(
87
- `[guardrails] skipping policy rule "${id}": missing non-empty patterns.`,
88
- );
89
- continue;
90
- }
91
-
92
- const normalizedAllowedPatterns = (rule.allowedPatterns ?? []).filter(
93
- (pattern) => pattern.pattern.trim().length > 0,
94
- );
95
-
96
- compiled.push({
97
- id,
98
- protection: rule.protection,
99
- patterns: compileFilePatterns(normalizedPatterns),
100
- allowedPatterns: compileFilePatterns(normalizedAllowedPatterns),
101
- onlyIfExists: rule.onlyIfExists ?? true,
102
- blockMessage:
103
- rule.blockMessage ?? DEFAULT_BLOCK_MESSAGES[rule.protection] ?? "",
104
- enabled: rule.enabled ?? true,
105
- });
106
- }
107
-
108
- return compiled;
109
- }
110
-
111
- function normalizeTargetForPolicy(filePath: string, cwd: string): string {
112
- if (filePath === "~" || filePath.startsWith("~/")) {
113
- return normalizeFilePath(filePath);
114
- }
115
-
116
- const expanded = expandHomePath(filePath);
117
- const absolute = resolve(cwd, expanded);
118
- const rel = relative(cwd, absolute);
119
- const normalizedHome = normalizeFilePath(expandHomePath("~"));
120
- const normalizedAbsolute = normalizeFilePath(absolute);
121
-
122
- if (normalizedAbsolute.startsWith(`${normalizedHome}/`)) {
123
- return normalizeFilePath(`~/${relative(expandHomePath("~"), absolute)}`);
124
- }
125
-
126
- const candidate =
127
- rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : absolute;
128
-
129
- return normalizeFilePath(candidate);
130
- }
131
-
132
- function resolvePolicyPath(filePath: string, cwd: string): string {
133
- return resolve(cwd, expandHomePath(filePath));
134
- }
135
-
136
- function matchesAnyPolicyPattern(
137
- filePath: string,
138
- rules: CompiledRule[],
139
- ): boolean {
140
- return rules.some(
141
- (rule) =>
142
- rule.enabled && rule.patterns.some((pattern) => pattern.test(filePath)),
143
- );
144
- }
145
-
146
- async function expandCandidate(candidate: string): Promise<string[]> {
147
- if (!hasGlobChars(candidate)) return [candidate];
148
-
149
- const matches = await expandGlob(candidate);
150
- if (matches.length > 0) return matches;
151
-
152
- return [candidate];
153
- }
154
-
155
- async function extractBashFileTargets(
156
- command: string,
157
- rules: CompiledRule[],
158
- cwd: string,
159
- ): Promise<string[]> {
160
- const targets = new Set<string>();
161
-
162
- const maybeAddTarget = async (candidate: string): Promise<void> => {
163
- if (!candidate || candidate.startsWith("-")) return;
164
-
165
- const expanded = await expandCandidate(candidate);
166
- for (const file of expanded) {
167
- const normalized = normalizeTargetForPolicy(file, cwd);
168
- if (matchesAnyPolicyPattern(normalized, rules)) {
169
- targets.add(normalized);
170
- }
171
- }
172
- };
173
-
174
- try {
175
- const { ast } = parse(command);
176
- const pending: Promise<void>[] = [];
177
-
178
- walkCommands(ast, (cmd) => {
179
- const words = (cmd.words ?? []).map(wordToString);
180
- for (let i = 1; i < words.length; i++) {
181
- const arg = words[i] as string;
182
- pending.push(maybeAddTarget(arg));
183
- }
184
-
185
- for (const redir of cmd.redirects ?? []) {
186
- const target = wordToString(redir.target);
187
- pending.push(maybeAddTarget(target));
188
- }
189
-
190
- return false;
191
- });
192
-
193
- await Promise.all(pending);
194
-
195
- return [...targets];
196
- } catch {
197
- const tokenRegex = /"([^"]+)"|'([^']+)'|`([^`]+)`|([^\s"'`<>|;&]+)/g;
198
-
199
- for (const match of command.matchAll(tokenRegex)) {
200
- const token = match[1] ?? match[2] ?? match[3] ?? match[4] ?? "";
201
- if (!token || token.startsWith("-") || !maybePathLike(token)) {
202
- continue;
203
- }
204
-
205
- const expanded = await expandCandidate(token);
206
- for (const file of expanded) {
207
- const normalized = normalizeTargetForPolicy(file, cwd);
208
- if (matchesAnyPolicyPattern(normalized, rules)) {
209
- targets.add(normalized);
210
- }
211
- }
212
- }
213
-
214
- return [...targets];
215
- }
216
- }
217
-
218
- async function getEffectiveProtection(
219
- filePath: string,
220
- compiledRules: CompiledRule[],
221
- cwd: string,
222
- ): Promise<{
223
- protection: Protection;
224
- blockMessage: string;
225
- ruleId: string;
226
- } | null> {
227
- let bestMatch: {
228
- protection: Protection;
229
- blockMessage: string;
230
- ruleId: string;
231
- rank: number;
232
- } | null = null;
233
-
234
- for (const rule of compiledRules) {
235
- if (!rule.enabled) continue;
236
-
237
- const matched = rule.patterns.some((pattern) => pattern.test(filePath));
238
- if (!matched) continue;
239
-
240
- const allowed = rule.allowedPatterns.some((pattern) =>
241
- pattern.test(filePath),
242
- );
243
- if (allowed) continue;
244
-
245
- if (rule.onlyIfExists && !(await fileExists(filePath, cwd))) continue;
246
-
247
- const rank = protectionRank(rule.protection);
248
- if (!bestMatch || rank > bestMatch.rank) {
249
- bestMatch = {
250
- protection: rule.protection,
251
- blockMessage: rule.blockMessage,
252
- ruleId: rule.id,
253
- rank,
254
- };
255
- }
256
- }
257
-
258
- if (!bestMatch || bestMatch.protection === "none") return null;
259
-
260
- return {
261
- protection: bestMatch.protection,
262
- blockMessage: bestMatch.blockMessage,
263
- ruleId: bestMatch.ruleId,
264
- };
265
- }
266
-
267
- function extractPathTarget(input: Record<string, unknown>): string[] {
268
- const target = String(input.file_path ?? input.path ?? "").trim();
269
- return target ? [target] : [];
270
- }
271
-
272
- export function setupPoliciesHook(pi: ExtensionAPI, config: ResolvedConfig) {
273
- if (!config.features.policies) return;
274
-
275
- const compiledRules = compileRules(config.policies.rules);
276
-
277
- pi.on("tool_call", async (event, ctx) => {
278
- const toolName = event.toolName;
279
- let targets: string[] = [];
280
-
281
- if (["read", "write", "edit", "grep", "find", "ls"].includes(toolName)) {
282
- targets = extractPathTarget(event.input);
283
- } else if (toolName === "bash") {
284
- const command = String(event.input.command ?? "");
285
- targets = await extractBashFileTargets(command, compiledRules, ctx.cwd);
286
- } else {
287
- return;
288
- }
289
-
290
- for (const target of targets) {
291
- const normalizedTarget = normalizeTargetForPolicy(target, ctx.cwd);
292
-
293
- const effective = await getEffectiveProtection(
294
- normalizedTarget,
295
- compiledRules,
296
- ctx.cwd,
297
- );
298
- if (!effective) continue;
299
-
300
- const blockedTools = BLOCKED_TOOLS[effective.protection];
301
- if (!blockedTools.has(toolName)) continue;
302
-
303
- ctx.ui.notify(
304
- `Blocked ${toolName} on protected file: ${normalizedTarget} (${effective.ruleId})`,
305
- "warning",
306
- );
307
-
308
- const reason = effective.blockMessage.replace("{file}", normalizedTarget);
309
-
310
- emitBlocked(pi, {
311
- feature: "policies",
312
- toolName,
313
- input: event.input,
314
- reason,
315
- });
316
-
317
- return { block: true, reason };
318
- }
319
-
320
- return;
321
- });
322
- }
package/src/index.ts DELETED
@@ -1,96 +0,0 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import { isOnboardingPending } from "./commands/onboarding";
3
- import { registerGuardrailsOnboardingCommand } from "./commands/onboarding-command";
4
- import { registerGuardrailsSettings } from "./commands/settings-command";
5
- import { configLoader } from "./config";
6
- import { setupGuardrailsHooks } from "./hooks";
7
- import {
8
- migrateApplyBuiltinDefaults,
9
- migrateMarkOnboardingDone,
10
- needsApplyBuiltinDefaultsMigration,
11
- needsOnboardingDoneMigration,
12
- } from "./utils/migration";
13
- import { pendingWarnings } from "./utils/warnings";
14
-
15
- /**
16
- * Guardrails Extension
17
- *
18
- * Security hooks to prevent potentially dangerous operations:
19
- * - policies: File access policies with per-rule protection levels
20
- * - permission-gate: Prompts for confirmation on dangerous commands
21
- *
22
- * Toolchain features (preventBrew, preventPython, enforcePackageManager,
23
- * packageManager) have been moved to @aliou/pi-toolchain. Old configs
24
- * containing these fields are auto-migrated on first load.
25
- *
26
- * Configuration:
27
- * - Global: ~/.pi/agent/extensions/guardrails.json
28
- * - Project: .pi/extensions/guardrails.json
29
- * - Command: /guardrails:settings
30
- */
31
- export default async function (pi: ExtensionAPI) {
32
- await configLoader.load();
33
-
34
- const hasGlobalConfig = configLoader.hasConfig("global");
35
-
36
- if (hasGlobalConfig) {
37
- const globalConfig = configLoader.getRawConfig("global");
38
- if (globalConfig) {
39
- let migrated = globalConfig;
40
- let changed = false;
41
-
42
- if (needsApplyBuiltinDefaultsMigration(migrated)) {
43
- migrated = migrateApplyBuiltinDefaults(migrated);
44
- changed = true;
45
- }
46
-
47
- if (needsOnboardingDoneMigration(migrated)) {
48
- migrated = migrateMarkOnboardingDone(migrated);
49
- changed = true;
50
- }
51
-
52
- if (changed) {
53
- await configLoader.save("global", migrated);
54
- await configLoader.load();
55
- }
56
- }
57
- }
58
-
59
- let hooksRegistered = false;
60
-
61
- registerGuardrailsSettings(pi);
62
-
63
- const maybeRegisterHooks = () => {
64
- if (hooksRegistered) return;
65
- const config = configLoader.getConfig();
66
- if (!config.enabled) return;
67
- setupGuardrailsHooks(pi, config);
68
- hooksRegistered = true;
69
- };
70
-
71
- if (isOnboardingPending(configLoader.getRawConfig("global"))) {
72
- registerGuardrailsOnboardingCommand(pi, maybeRegisterHooks);
73
- } else {
74
- maybeRegisterHooks();
75
- }
76
-
77
- pi.on("session_start", (_event, ctx) => {
78
- for (const warning of pendingWarnings.splice(0)) {
79
- ctx.ui.notify(warning, "warning");
80
- }
81
-
82
- if (!ctx.hasUI) {
83
- return;
84
- }
85
-
86
- if (isOnboardingPending(configLoader.getRawConfig("global"))) {
87
- ctx.ui.notify(
88
- "[Guardrails] setup pending. Run `/guardrails:onboarding` to choose recommended or minimal protection defaults.",
89
- "info",
90
- );
91
- return;
92
- }
93
-
94
- maybeRegisterHooks();
95
- });
96
- }
@@ -1,280 +0,0 @@
1
- /**
2
- * Core subagent executor.
3
- *
4
- * Uses createAgentSession from the SDK for all subagent patterns.
5
- * Supports streaming text updates, tool execution tracking, and usage tracking.
6
- */
7
-
8
- import type { AssistantMessage } from "@mariozechner/pi-ai";
9
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
10
- import {
11
- createAgentSession,
12
- DefaultResourceLoader,
13
- getAgentDir,
14
- SessionManager,
15
- SettingsManager,
16
- } from "@mariozechner/pi-coding-agent";
17
- import {
18
- createExecutionTimer,
19
- markExecutionEnd,
20
- markExecutionStart,
21
- } from "./timing";
22
- import type {
23
- OnTextUpdate,
24
- OnToolUpdate,
25
- SubagentConfig,
26
- SubagentResult,
27
- SubagentToolCall,
28
- SubagentUsage,
29
- } from "./types";
30
-
31
- function generateRunId(name: string): string {
32
- const slug =
33
- name
34
- .trim()
35
- .toLowerCase()
36
- .replace(/[^a-z0-9]+/g, "-") || "subagent";
37
- const randomPart =
38
- typeof globalThis.crypto?.randomUUID === "function"
39
- ? globalThis.crypto.randomUUID().slice(0, 8)
40
- : Date.now().toString(36);
41
- return `${slug}-${randomPart}`;
42
- }
43
-
44
- /**
45
- * Execute a subagent with the given configuration.
46
- *
47
- * @param config - Subagent configuration
48
- * @param userMessage - The user's prompt
49
- * @param ctx - Extension context
50
- * @param onTextUpdate - Callback for streaming text
51
- * @param signal - Abort signal
52
- * @param onToolUpdate - Callback for tool execution updates
53
- */
54
- export async function executeSubagent(
55
- config: SubagentConfig,
56
- userMessage: string,
57
- ctx: ExtensionContext,
58
- onTextUpdate?: OnTextUpdate,
59
- signal?: AbortSignal,
60
- onToolUpdate?: OnToolUpdate,
61
- ): Promise<SubagentResult> {
62
- const runId = generateRunId(config.name);
63
- const executionTimer = createExecutionTimer();
64
-
65
- const agentDir = getAgentDir();
66
- const settingsManager = SettingsManager.create(ctx.cwd, agentDir);
67
- const resourceLoader = new DefaultResourceLoader({
68
- cwd: ctx.cwd,
69
- agentDir,
70
- settingsManager,
71
- noExtensions: true,
72
- noPromptTemplates: true,
73
- noThemes: true,
74
- noSkills: true,
75
- systemPromptOverride: () => config.systemPrompt,
76
- appendSystemPromptOverride: () => [],
77
- agentsFilesOverride: () => ({ agentsFiles: [] }),
78
- skillsOverride: () => ({
79
- skills: config.skills ?? [],
80
- diagnostics: [],
81
- }),
82
- });
83
- await resourceLoader.reload();
84
-
85
- const { session } = await createAgentSession({
86
- model: config.model,
87
- tools: config.tools ?? [],
88
- customTools: config.customTools ?? [],
89
- sessionManager: SessionManager.inMemory(),
90
- thinkingLevel: config.thinkingLevel ?? "low",
91
- modelRegistry: ctx.modelRegistry,
92
- resourceLoader,
93
- });
94
-
95
- let accumulated = "";
96
- let finalResponse = "";
97
- let aborted = false;
98
- const toolCalls = new Map<string, SubagentToolCall>();
99
-
100
- let toolsHaveStarted = false;
101
- let toolsHaveCompleted = false;
102
-
103
- const usage: SubagentUsage = {
104
- inputTokens: 0,
105
- outputTokens: 0,
106
- cacheReadTokens: 0,
107
- cacheWriteTokens: 0,
108
- estimatedTokens: 0,
109
- llmCost: 0,
110
- toolCost: 0,
111
- totalCost: 0,
112
- };
113
-
114
- const unsubscribe = session.subscribe((event) => {
115
- if (event.type === "message_update") {
116
- if (event.assistantMessageEvent.type === "text_delta") {
117
- const delta = event.assistantMessageEvent.delta;
118
- accumulated += delta;
119
-
120
- if (toolsHaveCompleted) {
121
- finalResponse += delta;
122
- }
123
-
124
- onTextUpdate?.(delta, accumulated);
125
- }
126
- }
127
-
128
- if (event.type === "tool_execution_start") {
129
- toolsHaveStarted = true;
130
- toolsHaveCompleted = false;
131
- finalResponse = "";
132
- const toolCall: SubagentToolCall = {
133
- toolCallId: event.toolCallId,
134
- toolName: event.toolName,
135
- args: event.args ?? {},
136
- status: "running",
137
- };
138
- markExecutionStart(toolCall);
139
- toolCalls.set(event.toolCallId, toolCall);
140
- onToolUpdate?.([...toolCalls.values()]);
141
- }
142
-
143
- if (event.type === "tool_execution_update") {
144
- const existing = toolCalls.get(event.toolCallId);
145
- if (existing) {
146
- existing.args = event.args ?? existing.args;
147
- if (event.partialResult) {
148
- existing.partialResult = event.partialResult as {
149
- content: Array<{ type: string; text?: string }>;
150
- details?: unknown;
151
- };
152
- }
153
- onToolUpdate?.([...toolCalls.values()]);
154
- }
155
- }
156
-
157
- if (event.type === "tool_execution_end") {
158
- const existing = toolCalls.get(event.toolCallId);
159
- if (existing) {
160
- existing.status = event.isError ? "error" : "done";
161
- existing.result = event.result;
162
- markExecutionEnd(existing);
163
- if (event.isError && event.result) {
164
- existing.error =
165
- typeof event.result === "string"
166
- ? event.result
167
- : JSON.stringify(event.result);
168
- }
169
- onToolUpdate?.([...toolCalls.values()]);
170
-
171
- const resultDetails = event.result?.details as
172
- | { cost?: number }
173
- | undefined;
174
- if (resultDetails?.cost !== undefined) {
175
- usage.toolCost = (usage.toolCost ?? 0) + resultDetails.cost;
176
- }
177
- }
178
-
179
- const allDone = [...toolCalls.values()].every(
180
- (tc) => tc.status === "done" || tc.status === "error",
181
- );
182
- if (allDone) {
183
- toolsHaveCompleted = true;
184
- }
185
- }
186
-
187
- if (event.type === "turn_end") {
188
- const msg = event.message;
189
- if (msg.role === "assistant") {
190
- const assistantMsg = msg as AssistantMessage;
191
- const msgUsage = assistantMsg.usage;
192
- if (msgUsage) {
193
- usage.inputTokens = (usage.inputTokens ?? 0) + msgUsage.input;
194
- usage.outputTokens = (usage.outputTokens ?? 0) + msgUsage.output;
195
- usage.cacheReadTokens =
196
- (usage.cacheReadTokens ?? 0) + msgUsage.cacheRead;
197
- usage.cacheWriteTokens =
198
- (usage.cacheWriteTokens ?? 0) + msgUsage.cacheWrite;
199
- usage.llmCost = (usage.llmCost ?? 0) + msgUsage.cost.total;
200
- }
201
- }
202
- }
203
- });
204
-
205
- if (signal) {
206
- if (signal.aborted) {
207
- unsubscribe();
208
- session.dispose();
209
- return {
210
- content: "",
211
- aborted: true,
212
- toolCalls: [],
213
- totalDurationMs: executionTimer.getDurationMs(),
214
- runId,
215
- usage,
216
- };
217
- }
218
-
219
- signal.addEventListener(
220
- "abort",
221
- () => {
222
- session.abort();
223
- aborted = true;
224
- },
225
- { once: true },
226
- );
227
- }
228
-
229
- let error: string | undefined;
230
-
231
- try {
232
- await session.prompt(userMessage);
233
- } catch (err) {
234
- if (signal?.aborted) {
235
- aborted = true;
236
- } else {
237
- error =
238
- err instanceof Error
239
- ? err.message
240
- : typeof err === "string"
241
- ? err
242
- : JSON.stringify(err);
243
- }
244
- } finally {
245
- unsubscribe();
246
- session.dispose();
247
- }
248
-
249
- const responseText = toolsHaveStarted ? finalResponse : accumulated;
250
- const cleanedContent = filterThinkingTags(responseText);
251
-
252
- const totalRealTokens =
253
- (usage.inputTokens ?? 0) +
254
- (usage.outputTokens ?? 0) +
255
- (usage.cacheReadTokens ?? 0) +
256
- (usage.cacheWriteTokens ?? 0);
257
- usage.estimatedTokens =
258
- totalRealTokens > 0
259
- ? totalRealTokens
260
- : Math.round(cleanedContent.length / 4);
261
-
262
- usage.totalCost = (usage.llmCost ?? 0) + (usage.toolCost ?? 0);
263
-
264
- return {
265
- content: cleanedContent,
266
- aborted,
267
- toolCalls: [...toolCalls.values()],
268
- totalDurationMs: executionTimer.getDurationMs(),
269
- error,
270
- runId,
271
- usage,
272
- };
273
- }
274
-
275
- /**
276
- * Filter out <thinking>...</thinking> tags from text.
277
- */
278
- export function filterThinkingTags(text: string): string {
279
- return text.replace(/<thinking>[\s\S]*?<\/thinking>\s*/g, "");
280
- }
package/src/lib/index.ts DELETED
@@ -1,16 +0,0 @@
1
- export { executeSubagent, filterThinkingTags } from "./executor";
2
- export { resolveModel } from "./model-resolver";
3
- export {
4
- createExecutionTimer,
5
- markExecutionEnd,
6
- markExecutionStart,
7
- type TimedExecution,
8
- } from "./timing";
9
- export type {
10
- OnTextUpdate,
11
- OnToolUpdate,
12
- SubagentConfig,
13
- SubagentResult,
14
- SubagentToolCall,
15
- SubagentUsage,
16
- } from "./types";