@asterxsk/kiln 0.1.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.
- package/LICENSE +21 -0
- package/README.md +170 -0
- package/agent/AGENTS.md +67 -0
- package/agent/README.md +5 -0
- package/agent/extensions/AGENTS.md +68 -0
- package/agent/extensions/ask-user/index.ts +418 -0
- package/agent/extensions/ask-user/install.ps1 +23 -0
- package/agent/extensions/ask-user/install.sh +21 -0
- package/agent/extensions/ask-user/package-lock.json +769 -0
- package/agent/extensions/ask-user/package.json +19 -0
- package/agent/extensions/ask-user/prompt.ts +45 -0
- package/agent/extensions/ask-user/tsconfig.json +7 -0
- package/agent/extensions/background-terminals/docs/implementation-guide.md +942 -0
- package/agent/extensions/background-terminals/index.ts +627 -0
- package/agent/extensions/background-terminals/install.ps1 +23 -0
- package/agent/extensions/background-terminals/install.sh +21 -0
- package/agent/extensions/background-terminals/manager.test.ts +735 -0
- package/agent/extensions/background-terminals/output.test.ts +109 -0
- package/agent/extensions/background-terminals/package-lock.json +769 -0
- package/agent/extensions/background-terminals/package.json +17 -0
- package/agent/extensions/background-terminals/prompt.test.ts +125 -0
- package/agent/extensions/background-terminals/ps.test.ts +82 -0
- package/agent/extensions/background-terminals/result-delivery.test.ts +44 -0
- package/agent/extensions/background-terminals/src/domain.ts +87 -0
- package/agent/extensions/background-terminals/src/manager.ts +907 -0
- package/agent/extensions/background-terminals/src/output.ts +84 -0
- package/agent/extensions/background-terminals/src/prompt.ts +142 -0
- package/agent/extensions/background-terminals/src/result-delivery.ts +27 -0
- package/agent/extensions/background-terminals/src/runtime.ts +36 -0
- package/agent/extensions/background-terminals/src/ui/output-view.ts +79 -0
- package/agent/extensions/background-terminals/src/ui/ps.ts +621 -0
- package/agent/extensions/background-terminals/tsconfig.json +7 -0
- package/agent/extensions/destructive/README.md +31 -0
- package/agent/extensions/destructive/index.ts +88 -0
- package/agent/extensions/destructive/install.ps1 +23 -0
- package/agent/extensions/destructive/install.sh +21 -0
- package/agent/extensions/file-search/index.spec.ts +443 -0
- package/agent/extensions/file-search/index.ts +459 -0
- package/agent/extensions/file-search/install.ps1 +23 -0
- package/agent/extensions/file-search/install.sh +21 -0
- package/agent/extensions/file-search/package-lock.json +2253 -0
- package/agent/extensions/file-search/package.json +23 -0
- package/agent/extensions/file-search/src/args.ts +122 -0
- package/agent/extensions/file-search/src/binaries.ts +422 -0
- package/agent/extensions/file-search/src/output.ts +126 -0
- package/agent/extensions/file-search/src/process.ts +146 -0
- package/agent/extensions/file-search/src/prompt.ts +52 -0
- package/agent/extensions/file-search/tsconfig.json +7 -0
- package/agent/extensions/goal/README.md +50 -0
- package/agent/extensions/goal/index.ts +155 -0
- package/agent/extensions/goal/install.ps1 +23 -0
- package/agent/extensions/goal/install.sh +21 -0
- package/agent/extensions/modelconf/PLAN.md +915 -0
- package/agent/extensions/modelconf/README.md +66 -0
- package/agent/extensions/modelconf/index.ts +296 -0
- package/agent/extensions/modelconf/install.ps1 +23 -0
- package/agent/extensions/modelconf/install.sh +21 -0
- package/agent/extensions/modelconf/package-lock.json +1809 -0
- package/agent/extensions/modelconf/package.json +13 -0
- package/agent/extensions/modelconf/src/fuzzy.ts +26 -0
- package/agent/extensions/modelconf/src/glob.ts +13 -0
- package/agent/extensions/modelconf/src/persistence.test.ts +46 -0
- package/agent/extensions/modelconf/src/persistence.ts +164 -0
- package/agent/extensions/modelconf/src/ui/ModelConfView.test.ts +75 -0
- package/agent/extensions/modelconf/src/ui/ModelConfView.ts +1101 -0
- package/agent/extensions/modelconf/tsconfig.json +15 -0
- package/agent/extensions/pi-web-access/CHANGELOG.md +690 -0
- package/agent/extensions/pi-web-access/LICENSE +21 -0
- package/agent/extensions/pi-web-access/README.md +470 -0
- package/agent/extensions/pi-web-access/SECURITY.md +5 -0
- package/agent/extensions/pi-web-access/activity.ts +101 -0
- package/agent/extensions/pi-web-access/auth-fetch.ts +148 -0
- package/agent/extensions/pi-web-access/banner.png +0 -0
- package/agent/extensions/pi-web-access/brightdata-unlocker.ts +272 -0
- package/agent/extensions/pi-web-access/chrome-cookies.ts +669 -0
- package/agent/extensions/pi-web-access/content-find.ts +139 -0
- package/agent/extensions/pi-web-access/credential-source.ts +191 -0
- package/agent/extensions/pi-web-access/data-uri-sanitize.ts +406 -0
- package/agent/extensions/pi-web-access/datalab-pdf-extract.ts +568 -0
- package/agent/extensions/pi-web-access/declared-web-links.ts +173 -0
- package/agent/extensions/pi-web-access/evidence/CONTRACT-EVIDENCE.md +496 -0
- package/agent/extensions/pi-web-access/evidence/contract-probe.mjs +140 -0
- package/agent/extensions/pi-web-access/exa.ts +526 -0
- package/agent/extensions/pi-web-access/extract.ts +1196 -0
- package/agent/extensions/pi-web-access/feature-config.ts +29 -0
- package/agent/extensions/pi-web-access/fetch-params.ts +111 -0
- package/agent/extensions/pi-web-access/gemini-adc.ts +298 -0
- package/agent/extensions/pi-web-access/gemini-api.ts +353 -0
- package/agent/extensions/pi-web-access/gemini-pdf-extract.ts +108 -0
- package/agent/extensions/pi-web-access/gemini-search.ts +21 -0
- package/agent/extensions/pi-web-access/gemini-url-context.ts +128 -0
- package/agent/extensions/pi-web-access/gemini-web-config.ts +101 -0
- package/agent/extensions/pi-web-access/gemini-web.ts +487 -0
- package/agent/extensions/pi-web-access/github-api.ts +197 -0
- package/agent/extensions/pi-web-access/github-extract.ts +746 -0
- package/agent/extensions/pi-web-access/github-issue-pr.ts +700 -0
- package/agent/extensions/pi-web-access/index.ts +1737 -0
- package/agent/extensions/pi-web-access/package-lock.json +5808 -0
- package/agent/extensions/pi-web-access/package.json +64 -0
- package/agent/extensions/pi-web-access/page-query.ts +96 -0
- package/agent/extensions/pi-web-access/pdf-extract.ts +409 -0
- package/agent/extensions/pi-web-access/pi-web-fetch-demo.mp4 +0 -0
- package/agent/extensions/pi-web-access/promise-try.d.ts +7 -0
- package/agent/extensions/pi-web-access/query-rewrite.ts +51 -0
- package/agent/extensions/pi-web-access/render-search-error.ts +170 -0
- package/agent/extensions/pi-web-access/rsc-extract.ts +338 -0
- package/agent/extensions/pi-web-access/search-types.ts +20 -0
- package/agent/extensions/pi-web-access/source-check.ts +282 -0
- package/agent/extensions/pi-web-access/ssrf-protection.ts +526 -0
- package/agent/extensions/pi-web-access/storage.ts +521 -0
- package/agent/extensions/pi-web-access/summary-model-scope.ts +125 -0
- package/agent/extensions/pi-web-access/test/auth-fetch.test.mjs +208 -0
- package/agent/extensions/pi-web-access/test/brightdata-unlocker.test.mjs +840 -0
- package/agent/extensions/pi-web-access/test/chrome-cookie-extraction.test.mjs +441 -0
- package/agent/extensions/pi-web-access/test/config-path.test.mjs +283 -0
- package/agent/extensions/pi-web-access/test/content-find.test.mjs +25 -0
- package/agent/extensions/pi-web-access/test/credential-source.test.mjs +118 -0
- package/agent/extensions/pi-web-access/test/data-uri-sanitize.test.mjs +210 -0
- package/agent/extensions/pi-web-access/test/datalab-pdf-extract.test.mjs +552 -0
- package/agent/extensions/pi-web-access/test/declared-web-links.test.mjs +212 -0
- package/agent/extensions/pi-web-access/test/fetch-answer-storage.test.mjs +40 -0
- package/agent/extensions/pi-web-access/test/fetch-cache-storage.test.mjs +334 -0
- package/agent/extensions/pi-web-access/test/fetch-content-domain-policy.test.mjs +95 -0
- package/agent/extensions/pi-web-access/test/fetch-modes.test.mjs +53 -0
- package/agent/extensions/pi-web-access/test/fetch-not-found-guidance.test.mjs +92 -0
- package/agent/extensions/pi-web-access/test/fetch-params.test.mjs +86 -0
- package/agent/extensions/pi-web-access/test/fetch-render-call.test.mjs +34 -0
- package/agent/extensions/pi-web-access/test/fetch-routing.test.mjs +173 -0
- package/agent/extensions/pi-web-access/test/gemini-adc-auth.test.mjs +257 -0
- package/agent/extensions/pi-web-access/test/gemini-api-transport.test.mjs +170 -0
- package/agent/extensions/pi-web-access/test/gemini-pdf-extract.test.mjs +133 -0
- package/agent/extensions/pi-web-access/test/gemini-web-cookie-opt-in.test.mjs +178 -0
- package/agent/extensions/pi-web-access/test/gemini-web-header-overflow.test.mjs +148 -0
- package/agent/extensions/pi-web-access/test/get-search-content.test.mjs +223 -0
- package/agent/extensions/pi-web-access/test/github-extract.test.mjs +378 -0
- package/agent/extensions/pi-web-access/test/github-issue-pr.test.mjs +565 -0
- package/agent/extensions/pi-web-access/test/inline-content-config.test.mjs +99 -0
- package/agent/extensions/pi-web-access/test/lazy-extract-load.test.mjs +118 -0
- package/agent/extensions/pi-web-access/test/local-video-oversize.test.mjs +52 -0
- package/agent/extensions/pi-web-access/test/package-typebox-dependency.test.mjs +50 -0
- package/agent/extensions/pi-web-access/test/page-query.test.mjs +51 -0
- package/agent/extensions/pi-web-access/test/pdf-config.test.mjs +140 -0
- package/agent/extensions/pi-web-access/test/pdf-extract.test.mjs +500 -0
- package/agent/extensions/pi-web-access/test/proxy-transport.test.mjs +286 -0
- package/agent/extensions/pi-web-access/test/query-rewrite.test.mjs +52 -0
- package/agent/extensions/pi-web-access/test/rsc-fallback.test.mjs +102 -0
- package/agent/extensions/pi-web-access/test/search-error-render.test.mjs +152 -0
- package/agent/extensions/pi-web-access/test/search-providers.test.mjs +274 -0
- package/agent/extensions/pi-web-access/test/source-check.test.mjs +179 -0
- package/agent/extensions/pi-web-access/test/ssrf-allow-ranges-config.test.mjs +205 -0
- package/agent/extensions/pi-web-access/test/ssrf-protection.test.mjs +456 -0
- package/agent/extensions/pi-web-access/test/summary-model-scope.test.mjs +106 -0
- package/agent/extensions/pi-web-access/test/tool-registration-config.test.mjs +182 -0
- package/agent/extensions/pi-web-access/test/web-search-answer-render.test.mjs +66 -0
- package/agent/extensions/pi-web-access/test/youtube-extract-errors.test.mjs +64 -0
- package/agent/extensions/pi-web-access/tsconfig.json +11 -0
- package/agent/extensions/pi-web-access/utils.ts +451 -0
- package/agent/extensions/pi-web-access/video-extract.ts +392 -0
- package/agent/extensions/pi-web-access/youtube-extract.ts +328 -0
- package/agent/extensions/shared/activity-status.ts +31 -0
- package/agent/extensions/shared/child-session.test.ts +270 -0
- package/agent/extensions/shared/child-session.ts +148 -0
- package/agent/extensions/shared/context-utilization.test.ts +48 -0
- package/agent/extensions/shared/context-utilization.ts +47 -0
- package/agent/extensions/shared/dashboard-state.ts +99 -0
- package/agent/extensions/shared/install.ps1 +23 -0
- package/agent/extensions/shared/install.sh +21 -0
- package/agent/extensions/shared/tool-call-timeout.test.ts +117 -0
- package/agent/extensions/shared/tool-call-timeout.ts +104 -0
- package/agent/extensions/skillsconf/README.md +74 -0
- package/agent/extensions/skillsconf/index.ts +92 -0
- package/agent/extensions/skillsconf/install.ps1 +23 -0
- package/agent/extensions/skillsconf/install.sh +21 -0
- package/agent/extensions/skillsconf/package-lock.json +1809 -0
- package/agent/extensions/skillsconf/package.json +18 -0
- package/agent/extensions/skillsconf/src/delete-skill.test.ts +64 -0
- package/agent/extensions/skillsconf/src/delete-skill.ts +45 -0
- package/agent/extensions/skillsconf/src/filter.test.ts +80 -0
- package/agent/extensions/skillsconf/src/filter.ts +74 -0
- package/agent/extensions/skillsconf/src/fuzzy.ts +26 -0
- package/agent/extensions/skillsconf/src/persistence.test.ts +72 -0
- package/agent/extensions/skillsconf/src/persistence.ts +169 -0
- package/agent/extensions/skillsconf/src/ui/SkillConfView.test.ts +337 -0
- package/agent/extensions/skillsconf/src/ui/SkillConfView.ts +758 -0
- package/agent/extensions/skillsconf/src/ui/text-input.ts +91 -0
- package/agent/extensions/skillsconf/src/ui/tui-helpers.ts +144 -0
- package/agent/extensions/skillsconf/tsconfig.json +15 -0
- package/agent/extensions/status line/index.ts +282 -0
- package/agent/extensions/status line/install.ps1 +23 -0
- package/agent/extensions/status line/install.sh +21 -0
- package/agent/extensions/subagents/by-the-way.test.ts +29 -0
- package/agent/extensions/subagents/claude.test.ts +119 -0
- package/agent/extensions/subagents/codex.test.ts +102 -0
- package/agent/extensions/subagents/context-usage.test.ts +107 -0
- package/agent/extensions/subagents/docs/design-plan.md +568 -0
- package/agent/extensions/subagents/docs/effect-v4-extension-guide.md +354 -0
- package/agent/extensions/subagents/docs/effect-v4-notes.md +571 -0
- package/agent/extensions/subagents/index.ts +779 -0
- package/agent/extensions/subagents/install.ps1 +23 -0
- package/agent/extensions/subagents/install.sh +21 -0
- package/agent/extensions/subagents/manager.test.ts +276 -0
- package/agent/extensions/subagents/package-lock.json +2244 -0
- package/agent/extensions/subagents/package.json +19 -0
- package/agent/extensions/subagents/result-delivery.test.ts +27 -0
- package/agent/extensions/subagents/src/backend.ts +73 -0
- package/agent/extensions/subagents/src/backends/claude.ts +701 -0
- package/agent/extensions/subagents/src/backends/codex.ts +1060 -0
- package/agent/extensions/subagents/src/backends/pi.ts +575 -0
- package/agent/extensions/subagents/src/backends/stub.ts +300 -0
- package/agent/extensions/subagents/src/by-the-way.ts +21 -0
- package/agent/extensions/subagents/src/domain.ts +253 -0
- package/agent/extensions/subagents/src/format.ts +74 -0
- package/agent/extensions/subagents/src/manager.ts +736 -0
- package/agent/extensions/subagents/src/prompt.ts +92 -0
- package/agent/extensions/subagents/src/result-delivery.ts +20 -0
- package/agent/extensions/subagents/src/runtime.ts +53 -0
- package/agent/extensions/subagents/src/ui/takeover.ts +583 -0
- package/agent/extensions/subagents/src/ui/transcript.ts +201 -0
- package/agent/extensions/subagents/takeover.test.ts +29 -0
- package/agent/extensions/subagents/tsconfig.json +7 -0
- package/agent/extensions/taste/index.ts +443 -0
- package/agent/extensions/taste/install.ps1 +23 -0
- package/agent/extensions/taste/install.sh +21 -0
- package/agent/extensions/todo/AGENTS.md +38 -0
- package/agent/extensions/todo/LICENSE +21 -0
- package/agent/extensions/todo/config.ts +55 -0
- package/agent/extensions/todo/index.ts +151 -0
- package/agent/extensions/todo/install.ps1 +23 -0
- package/agent/extensions/todo/install.sh +21 -0
- package/agent/extensions/todo/locales/de.json +17 -0
- package/agent/extensions/todo/locales/en.json +15 -0
- package/agent/extensions/todo/locales/es.json +17 -0
- package/agent/extensions/todo/locales/fr.json +17 -0
- package/agent/extensions/todo/locales/pt-BR.json +17 -0
- package/agent/extensions/todo/locales/pt.json +17 -0
- package/agent/extensions/todo/locales/ru.json +17 -0
- package/agent/extensions/todo/locales/uk.json +17 -0
- package/agent/extensions/todo/locales/zh.json +17 -0
- package/agent/extensions/todo/package-lock.json +3358 -0
- package/agent/extensions/todo/package.json +67 -0
- package/agent/extensions/todo/state/i18n-bridge.ts +64 -0
- package/agent/extensions/todo/state/invariants.ts +20 -0
- package/agent/extensions/todo/state/replay.ts +38 -0
- package/agent/extensions/todo/state/selectors.ts +107 -0
- package/agent/extensions/todo/state/state-reducer.ts +326 -0
- package/agent/extensions/todo/state/state.ts +18 -0
- package/agent/extensions/todo/state/store.ts +82 -0
- package/agent/extensions/todo/state/task-graph.ts +57 -0
- package/agent/extensions/todo/todo-overlay.ts +200 -0
- package/agent/extensions/todo/todo.ts +155 -0
- package/agent/extensions/todo/tool/response-envelope.ts +109 -0
- package/agent/extensions/todo/tool/types.ts +206 -0
- package/agent/extensions/todo/verify-ref-system.js +0 -0
- package/agent/extensions/todo/view/format.ts +177 -0
- package/agent/extensions/trim-context/README.md +54 -0
- package/agent/extensions/trim-context/index.ts +487 -0
- package/agent/extensions/trim-context/install.ps1 +23 -0
- package/agent/extensions/trim-context/install.sh +21 -0
- package/agent/install.ps1 +527 -0
- package/agent/install.sh +511 -0
- package/agent/keybindings.json +7 -0
- package/bin/kiln.js +359 -0
- package/package.json +21 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
DefaultResourceLoader,
|
|
4
|
+
getAgentDir,
|
|
5
|
+
ProjectTrustStore,
|
|
6
|
+
SettingsManager,
|
|
7
|
+
type AgentSession,
|
|
8
|
+
type SessionShutdownEvent,
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
|
|
11
|
+
const CHILD_SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
12
|
+
|
|
13
|
+
/** Tools that headless children must not receive. Everything else stays enabled. */
|
|
14
|
+
export const CHILD_EXCLUDED_TOOL_NAMES = [
|
|
15
|
+
"subagent_spawn",
|
|
16
|
+
"subagent_wait",
|
|
17
|
+
"subagent_cancel",
|
|
18
|
+
"subagent_check",
|
|
19
|
+
"subagent_list",
|
|
20
|
+
"workflow",
|
|
21
|
+
"ask_user",
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
/** Fresh SDK options avoid turning the denylist into an accidental allowlist. */
|
|
25
|
+
export function childToolPolicy() {
|
|
26
|
+
return { excludeTools: [...CHILD_EXCLUDED_TOOL_NAMES] };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ChildResourceOptions {
|
|
30
|
+
cwd: string;
|
|
31
|
+
projectTrusted: boolean;
|
|
32
|
+
appendSystemPrompt?: string[];
|
|
33
|
+
agentDir?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Load normal global/package resources and trust-gated project resources. */
|
|
37
|
+
export async function createChildResources(options: ChildResourceOptions) {
|
|
38
|
+
const agentDir = options.agentDir ?? getAgentDir();
|
|
39
|
+
const settingsManager = SettingsManager.create(options.cwd, agentDir, {
|
|
40
|
+
projectTrusted: options.projectTrusted,
|
|
41
|
+
});
|
|
42
|
+
const loader = new DefaultResourceLoader({
|
|
43
|
+
cwd: options.cwd,
|
|
44
|
+
agentDir,
|
|
45
|
+
settingsManager,
|
|
46
|
+
...(options.appendSystemPrompt
|
|
47
|
+
? { appendSystemPrompt: options.appendSystemPrompt }
|
|
48
|
+
: {}),
|
|
49
|
+
});
|
|
50
|
+
await loader.reload();
|
|
51
|
+
return { loader, settingsManager };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Same-directory children inherit the live parent decision. An alternate cwd
|
|
56
|
+
* is trusted only when Pi's persisted trust store explicitly trusts it (or a
|
|
57
|
+
* containing directory); unreadable/invalid trust data fails closed.
|
|
58
|
+
*/
|
|
59
|
+
export function resolveStandaloneChildProjectTrust(options: {
|
|
60
|
+
parentCwd: string;
|
|
61
|
+
childCwd: string;
|
|
62
|
+
parentTrusted: boolean;
|
|
63
|
+
agentDir?: string;
|
|
64
|
+
}) {
|
|
65
|
+
if (path.resolve(options.childCwd) === path.resolve(options.parentCwd)) {
|
|
66
|
+
return options.parentTrusted;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const trustStore = new ProjectTrustStore(options.agentDir ?? getAgentDir());
|
|
70
|
+
return trustStore.get(options.childCwd) === true;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Start child extension session hooks/resources in headless print mode. */
|
|
77
|
+
export async function bindChildSessionExtensions(
|
|
78
|
+
session: Pick<AgentSession, "bindExtensions">,
|
|
79
|
+
) {
|
|
80
|
+
await session.bindExtensions({ mode: "print" });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface ChildExtensionRunner {
|
|
84
|
+
hasHandlers(eventType: string): boolean;
|
|
85
|
+
emit(event: SessionShutdownEvent): Promise<unknown>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface DisposableChildSession {
|
|
89
|
+
readonly extensionRunner: ChildExtensionRunner;
|
|
90
|
+
dispose(): void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const childShutdowns = new WeakMap<object, Promise<void>>();
|
|
94
|
+
|
|
95
|
+
function waitBounded(operation: Promise<unknown>, timeoutMs: number) {
|
|
96
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
97
|
+
const timeout = new Promise<void>((resolve) => {
|
|
98
|
+
timer = setTimeout(resolve, timeoutMs);
|
|
99
|
+
});
|
|
100
|
+
return Promise.race([
|
|
101
|
+
operation.then(
|
|
102
|
+
() => undefined,
|
|
103
|
+
() => undefined,
|
|
104
|
+
),
|
|
105
|
+
timeout,
|
|
106
|
+
])
|
|
107
|
+
.catch(() => {})
|
|
108
|
+
.finally(() => {
|
|
109
|
+
if (timer) clearTimeout(timer);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Emit child session_shutdown once, then dispose once. Hook failures and a
|
|
115
|
+
* bounded hook deadline never prevent disposal.
|
|
116
|
+
*/
|
|
117
|
+
export function shutdownAndDisposeChildSession(
|
|
118
|
+
session: DisposableChildSession,
|
|
119
|
+
options: { timeoutMs?: number } = {},
|
|
120
|
+
) {
|
|
121
|
+
const existing = childShutdowns.get(session);
|
|
122
|
+
if (existing) return existing;
|
|
123
|
+
|
|
124
|
+
const shutdown = (async () => {
|
|
125
|
+
try {
|
|
126
|
+
if (session.extensionRunner.hasHandlers("session_shutdown")) {
|
|
127
|
+
await waitBounded(
|
|
128
|
+
session.extensionRunner.emit({
|
|
129
|
+
type: "session_shutdown",
|
|
130
|
+
reason: "quit",
|
|
131
|
+
}),
|
|
132
|
+
options.timeoutMs ?? CHILD_SHUTDOWN_TIMEOUT_MS,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
// Extension runner inspection/emission is best-effort during teardown.
|
|
137
|
+
} finally {
|
|
138
|
+
try {
|
|
139
|
+
session.dispose();
|
|
140
|
+
} catch {
|
|
141
|
+
// Disposal is terminal and must remain idempotent for callers.
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
})();
|
|
145
|
+
|
|
146
|
+
childShutdowns.set(session, shutdown);
|
|
147
|
+
return shutdown;
|
|
148
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
contextPercent,
|
|
5
|
+
formatContextUtilization,
|
|
6
|
+
} from "./context-utilization.ts";
|
|
7
|
+
|
|
8
|
+
test("formats current context occupancy against model capacity", () => {
|
|
9
|
+
assert.equal(
|
|
10
|
+
formatContextUtilization({ tokens: 26_040, contextWindow: 372_000 }),
|
|
11
|
+
"7%/372k",
|
|
12
|
+
);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("formats the latest post-compaction usage rather than prior cumulative usage", () => {
|
|
16
|
+
const latestUsage = { tokens: 18_000, contextWindow: 200_000 };
|
|
17
|
+
assert.equal(formatContextUtilization(latestUsage), "9%/200k");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("clamps over-capacity and nonsensical token values", () => {
|
|
21
|
+
assert.equal(
|
|
22
|
+
contextPercent({ tokens: 500_000, contextWindow: 200_000 }),
|
|
23
|
+
100,
|
|
24
|
+
);
|
|
25
|
+
assert.equal(
|
|
26
|
+
formatContextUtilization({
|
|
27
|
+
tokens: Number.POSITIVE_INFINITY,
|
|
28
|
+
contextWindow: 200_000,
|
|
29
|
+
}),
|
|
30
|
+
"?%/200k",
|
|
31
|
+
);
|
|
32
|
+
assert.equal(
|
|
33
|
+
formatContextUtilization({ tokens: -1, contextWindow: 200_000 }),
|
|
34
|
+
"?%/200k",
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("handles missing usage or capacity without NaN or Infinity", () => {
|
|
39
|
+
assert.equal(
|
|
40
|
+
formatContextUtilization({ tokens: null, contextWindow: 372_000 }),
|
|
41
|
+
"?%/372k",
|
|
42
|
+
);
|
|
43
|
+
assert.equal(formatContextUtilization({ tokens: 12_000 }), "");
|
|
44
|
+
assert.equal(
|
|
45
|
+
formatContextUtilization({ tokens: 12_000, contextWindow: 0 }),
|
|
46
|
+
"",
|
|
47
|
+
);
|
|
48
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Compact, defensive context-window utilization formatting for child agents. */
|
|
2
|
+
|
|
3
|
+
export interface ContextUtilization {
|
|
4
|
+
/** Current conversation context occupancy; null while it is unknown after compaction. */
|
|
5
|
+
tokens?: number | null;
|
|
6
|
+
/** Capacity of the model currently serving the conversation. */
|
|
7
|
+
contextWindow?: number | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function usableTokens(value: number | null | undefined) {
|
|
11
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
12
|
+
? value
|
|
13
|
+
: undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function usableCapacity(value: number | null | undefined) {
|
|
17
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
18
|
+
? value
|
|
19
|
+
: undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function contextPercent(usage: ContextUtilization) {
|
|
23
|
+
const tokens = usableTokens(usage.tokens);
|
|
24
|
+
const capacity = usableCapacity(usage.contextWindow);
|
|
25
|
+
if (tokens === undefined || capacity === undefined) return undefined;
|
|
26
|
+
return Math.round(Math.min(100, Math.max(0, (tokens / capacity) * 100)));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function formatCompactTokens(count: number) {
|
|
30
|
+
if (count < 1000) return Math.round(count).toString();
|
|
31
|
+
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
32
|
+
if (count < 1000000) return `${Math.round(count / 1000)}k`;
|
|
33
|
+
return `${(count / 1000000).toFixed(1)}M`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Render `%/capacity`. If occupancy is temporarily unknown (notably directly
|
|
38
|
+
* after compaction), retain the useful model capacity as `?%/capacity`. If no
|
|
39
|
+
* valid capacity is available, omit the statistic because no percentage can
|
|
40
|
+
* be computed.
|
|
41
|
+
*/
|
|
42
|
+
export function formatContextUtilization(usage: ContextUtilization) {
|
|
43
|
+
const capacity = usableCapacity(usage.contextWindow);
|
|
44
|
+
if (capacity === undefined) return "";
|
|
45
|
+
const percent = contextPercent(usage);
|
|
46
|
+
return `${percent === undefined ? "?" : percent}%/${formatCompactTokens(capacity)}`;
|
|
47
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export const MODEL_INFO_CHANNEL = "dashboard:model-info";
|
|
2
|
+
export const GIT_INFO_CHANNEL = "dashboard:git-info";
|
|
3
|
+
export const REFRESH_CHANNEL = "dashboard:refresh";
|
|
4
|
+
|
|
5
|
+
export interface ModelInfoState {
|
|
6
|
+
provider: string;
|
|
7
|
+
modelId: string;
|
|
8
|
+
modelName: string;
|
|
9
|
+
thinking: string;
|
|
10
|
+
contextTokens: number | null;
|
|
11
|
+
contextWindow: number;
|
|
12
|
+
contextPercent: number | null;
|
|
13
|
+
cost: number;
|
|
14
|
+
tokensPerSecond: number | null;
|
|
15
|
+
generating: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PullRequestInfo {
|
|
19
|
+
number: number;
|
|
20
|
+
url: string;
|
|
21
|
+
isDraft: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface GitInfoState {
|
|
25
|
+
isRepository: boolean;
|
|
26
|
+
branch: string | null;
|
|
27
|
+
changedFiles: number;
|
|
28
|
+
pullRequest: PullRequestInfo | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function emptyModelInfoState(): ModelInfoState {
|
|
32
|
+
return {
|
|
33
|
+
provider: "",
|
|
34
|
+
modelId: "no-model",
|
|
35
|
+
modelName: "No model",
|
|
36
|
+
thinking: "off",
|
|
37
|
+
contextTokens: null,
|
|
38
|
+
contextWindow: 0,
|
|
39
|
+
contextPercent: null,
|
|
40
|
+
cost: 0,
|
|
41
|
+
tokensPerSecond: null,
|
|
42
|
+
generating: false,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function emptyGitInfoState(): GitInfoState {
|
|
47
|
+
return {
|
|
48
|
+
isRepository: false,
|
|
49
|
+
branch: null,
|
|
50
|
+
changedFiles: 0,
|
|
51
|
+
pullRequest: null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
56
|
+
return typeof value === "object" && value !== null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isNullableNumber(value: unknown) {
|
|
60
|
+
return value === null || typeof value === "number";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isModelInfoState(value: unknown): value is ModelInfoState {
|
|
64
|
+
if (!isRecord(value)) return false;
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
typeof value.provider === "string" &&
|
|
68
|
+
typeof value.modelId === "string" &&
|
|
69
|
+
typeof value.modelName === "string" &&
|
|
70
|
+
typeof value.thinking === "string" &&
|
|
71
|
+
isNullableNumber(value.contextTokens) &&
|
|
72
|
+
typeof value.contextWindow === "number" &&
|
|
73
|
+
isNullableNumber(value.contextPercent) &&
|
|
74
|
+
typeof value.cost === "number" &&
|
|
75
|
+
isNullableNumber(value.tokensPerSecond) &&
|
|
76
|
+
typeof value.generating === "boolean"
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isPullRequestInfo(value: unknown): value is PullRequestInfo {
|
|
81
|
+
if (!isRecord(value)) return false;
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
typeof value.number === "number" &&
|
|
85
|
+
typeof value.url === "string" &&
|
|
86
|
+
typeof value.isDraft === "boolean"
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function isGitInfoState(value: unknown): value is GitInfoState {
|
|
91
|
+
if (!isRecord(value)) return false;
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
typeof value.isRepository === "boolean" &&
|
|
95
|
+
(value.branch === null || typeof value.branch === "string") &&
|
|
96
|
+
typeof value.changedFiles === "number" &&
|
|
97
|
+
(value.pullRequest === null || isPullRequestInfo(value.pullRequest))
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env pwsh
|
|
2
|
+
# Per-extension installer — Windows / PowerShell
|
|
3
|
+
# Installs npm dependencies for this extension. No-op if no package.json.
|
|
4
|
+
$ErrorActionPreference = "Stop"
|
|
5
|
+
$extDir = $PSScriptRoot
|
|
6
|
+
if (-not $extDir) { $extDir = Split-Path -Parent $MyInvocation.MyCommand.Path }
|
|
7
|
+
if (-not (Test-Path "$extDir/package.json")) {
|
|
8
|
+
Write-Host " - no package.json — skipping" -ForegroundColor DarkGray
|
|
9
|
+
exit 0
|
|
10
|
+
}
|
|
11
|
+
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
|
|
12
|
+
Write-Host " x npm not found — install Node.js 22+" -ForegroundColor Red
|
|
13
|
+
exit 1
|
|
14
|
+
}
|
|
15
|
+
if (Test-Path "$extDir/package-lock.json") {
|
|
16
|
+
Write-Host " - npm ci" -ForegroundColor DarkGray
|
|
17
|
+
npm --prefix $extDir ci --no-audit --no-fund
|
|
18
|
+
} else {
|
|
19
|
+
Write-Host " - npm install" -ForegroundColor DarkGray
|
|
20
|
+
npm --prefix $extDir install --no-audit --no-fund
|
|
21
|
+
}
|
|
22
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
23
|
+
Write-Host " + done" -ForegroundColor Green
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Per-extension installer — macOS / Linux
|
|
3
|
+
# Installs npm dependencies for this extension. No-op if no package.json.
|
|
4
|
+
set -eu
|
|
5
|
+
EXT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
6
|
+
if [ ! -f "$EXT_DIR/package.json" ]; then
|
|
7
|
+
echo " · no package.json — skipping"
|
|
8
|
+
exit 0
|
|
9
|
+
fi
|
|
10
|
+
if ! command -v npm >/dev/null 2>&1; then
|
|
11
|
+
echo " ✖ npm not found — install Node.js 22+" >&2
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
if [ -f "$EXT_DIR/package-lock.json" ]; then
|
|
15
|
+
echo " · npm ci"
|
|
16
|
+
npm --prefix "$EXT_DIR" ci --no-audit --no-fund
|
|
17
|
+
else
|
|
18
|
+
echo " · npm install"
|
|
19
|
+
npm --prefix "$EXT_DIR" install --no-audit --no-fund
|
|
20
|
+
fi
|
|
21
|
+
echo " ✓ done"
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { Type } from "typebox";
|
|
5
|
+
import {
|
|
6
|
+
createToolCallTimeoutGuard,
|
|
7
|
+
runWithToolCallTimeout,
|
|
8
|
+
CHILD_TOOL_CALL_TIMEOUT_MS,
|
|
9
|
+
ToolCallTimeoutError,
|
|
10
|
+
} from "./tool-call-timeout.ts";
|
|
11
|
+
|
|
12
|
+
test("the production timeout error names the tool and three-minute limit", () => {
|
|
13
|
+
assert.equal(
|
|
14
|
+
new ToolCallTimeoutError("fixture_tool", CHILD_TOOL_CALL_TIMEOUT_MS)
|
|
15
|
+
.message,
|
|
16
|
+
'Tool call "fixture_tool" timed out after 3 minutes.',
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("a hung tool call fails clearly and receives an abort signal", async () => {
|
|
21
|
+
let executionSignal: AbortSignal | undefined;
|
|
22
|
+
|
|
23
|
+
await assert.rejects(
|
|
24
|
+
runWithToolCallTimeout("hung_fixture", 10, undefined, (signal) => {
|
|
25
|
+
executionSignal = signal;
|
|
26
|
+
return new Promise(() => {});
|
|
27
|
+
}),
|
|
28
|
+
(error: unknown) => {
|
|
29
|
+
assert.equal(error instanceof ToolCallTimeoutError, true);
|
|
30
|
+
assert.equal(
|
|
31
|
+
error instanceof Error ? error.message : "",
|
|
32
|
+
'Tool call "hung_fixture" timed out after 10 ms.',
|
|
33
|
+
);
|
|
34
|
+
return true;
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
assert.equal(executionSignal?.aborted, true);
|
|
39
|
+
assert.equal(executionSignal?.reason instanceof ToolCallTimeoutError, true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("parent cancellation still stops the timeout wrapper immediately", async () => {
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const reason = new Error("cancelled fixture");
|
|
45
|
+
const pending = runWithToolCallTimeout(
|
|
46
|
+
"hung_fixture",
|
|
47
|
+
60_000,
|
|
48
|
+
controller.signal,
|
|
49
|
+
() => new Promise(() => {}),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
controller.abort(reason);
|
|
53
|
+
|
|
54
|
+
await assert.rejects(pending, (error: unknown) => error === reason);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("the guard wraps each definition once and can discover later tools", () => {
|
|
58
|
+
const definitions = new Map<string, ToolDefinition>();
|
|
59
|
+
const createDefinition = (name: string): ToolDefinition => ({
|
|
60
|
+
name,
|
|
61
|
+
label: name,
|
|
62
|
+
description: name,
|
|
63
|
+
parameters: Type.Object({}),
|
|
64
|
+
async execute() {
|
|
65
|
+
return { content: [{ type: "text", text: "done" }], details: {} };
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
const first = createDefinition("first");
|
|
69
|
+
definitions.set(first.name, first);
|
|
70
|
+
const registry = {
|
|
71
|
+
getAllTools: () => [...definitions.keys()].map((name) => ({ name })),
|
|
72
|
+
getToolDefinition: (name: string) => definitions.get(name),
|
|
73
|
+
};
|
|
74
|
+
const guard = createToolCallTimeoutGuard(10);
|
|
75
|
+
|
|
76
|
+
const firstExecute = first.execute;
|
|
77
|
+
guard.apply(registry);
|
|
78
|
+
const firstWrappedExecute = first.execute;
|
|
79
|
+
assert.notEqual(firstWrappedExecute, firstExecute);
|
|
80
|
+
|
|
81
|
+
const second = createDefinition("second");
|
|
82
|
+
const secondExecute = second.execute;
|
|
83
|
+
definitions.set(second.name, second);
|
|
84
|
+
guard.apply(registry);
|
|
85
|
+
|
|
86
|
+
assert.equal(first.execute, firstWrappedExecute);
|
|
87
|
+
assert.notEqual(second.execute, secondExecute);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("successful and terminating tool results pass through unchanged", async () => {
|
|
91
|
+
const result = {
|
|
92
|
+
content: [{ type: "text" as const, text: "recorded" }],
|
|
93
|
+
details: { value: "fixture" },
|
|
94
|
+
terminate: true,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
assert.equal(
|
|
98
|
+
await runWithToolCallTimeout(
|
|
99
|
+
"structured_output",
|
|
100
|
+
10,
|
|
101
|
+
undefined,
|
|
102
|
+
async () => result,
|
|
103
|
+
),
|
|
104
|
+
result,
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("the timeout is fresh for each tool call, not shared across calls", async () => {
|
|
109
|
+
const execute = () =>
|
|
110
|
+
runWithToolCallTimeout("slow_fixture", 100, undefined, async () => {
|
|
111
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
112
|
+
return "done";
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
assert.equal(await execute(), "done");
|
|
116
|
+
assert.equal(await execute(), "done");
|
|
117
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export const CHILD_TOOL_CALL_TIMEOUT_MS = 3 * 60 * 1_000;
|
|
4
|
+
|
|
5
|
+
interface ToolRegistry {
|
|
6
|
+
getAllTools(): Array<{ name: string }>;
|
|
7
|
+
getToolDefinition(name: string): ToolDefinition | undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function formatTimeout(timeoutMs: number) {
|
|
11
|
+
if (timeoutMs % 60_000 === 0) {
|
|
12
|
+
const minutes = timeoutMs / 60_000;
|
|
13
|
+
return `${minutes} minute${minutes === 1 ? "" : "s"}`;
|
|
14
|
+
}
|
|
15
|
+
if (timeoutMs % 1_000 === 0) {
|
|
16
|
+
const seconds = timeoutMs / 1_000;
|
|
17
|
+
return `${seconds} second${seconds === 1 ? "" : "s"}`;
|
|
18
|
+
}
|
|
19
|
+
return `${timeoutMs} ms`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class ToolCallTimeoutError extends Error {
|
|
23
|
+
constructor(toolName: string, timeoutMs: number) {
|
|
24
|
+
super(
|
|
25
|
+
`Tool call "${toolName}" timed out after ${formatTimeout(timeoutMs)}.`,
|
|
26
|
+
);
|
|
27
|
+
this.name = "ToolCallTimeoutError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function runWithToolCallTimeout<T>(
|
|
32
|
+
toolName: string,
|
|
33
|
+
timeoutMs: number,
|
|
34
|
+
signal: AbortSignal | undefined,
|
|
35
|
+
execute: (signal: AbortSignal) => Promise<T>,
|
|
36
|
+
) {
|
|
37
|
+
const timeoutController = new AbortController();
|
|
38
|
+
const executionSignal = signal
|
|
39
|
+
? AbortSignal.any([signal, timeoutController.signal])
|
|
40
|
+
: timeoutController.signal;
|
|
41
|
+
const timeoutError = new ToolCallTimeoutError(toolName, timeoutMs);
|
|
42
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
43
|
+
const timeout = new Promise<never>((_resolve, reject) => {
|
|
44
|
+
timer = setTimeout(() => {
|
|
45
|
+
reject(timeoutError);
|
|
46
|
+
timeoutController.abort(timeoutError);
|
|
47
|
+
}, timeoutMs);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
let removeAbortListener: (() => void) | undefined;
|
|
51
|
+
const aborted = new Promise<never>((_resolve, reject) => {
|
|
52
|
+
if (!signal) return;
|
|
53
|
+
const onAbort = () => {
|
|
54
|
+
reject(
|
|
55
|
+
signal.reason instanceof Error
|
|
56
|
+
? signal.reason
|
|
57
|
+
: new Error(`Tool call "${toolName}" was aborted.`),
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
if (signal.aborted) {
|
|
61
|
+
onAbort();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
65
|
+
removeAbortListener = () => signal.removeEventListener("abort", onAbort);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
return await Promise.race([execute(executionSignal), timeout, aborted]);
|
|
70
|
+
} finally {
|
|
71
|
+
if (timer) clearTimeout(timer);
|
|
72
|
+
removeAbortListener?.();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Wrap every currently registered child tool with an independent execution
|
|
78
|
+
* timeout. Calling apply() again is safe and picks up tools registered later.
|
|
79
|
+
*/
|
|
80
|
+
export function createToolCallTimeoutGuard(
|
|
81
|
+
timeoutMs = CHILD_TOOL_CALL_TIMEOUT_MS,
|
|
82
|
+
) {
|
|
83
|
+
const wrapped = new WeakSet<ToolDefinition>();
|
|
84
|
+
|
|
85
|
+
const wrap = (definition: ToolDefinition) => {
|
|
86
|
+
if (wrapped.has(definition)) return;
|
|
87
|
+
wrapped.add(definition);
|
|
88
|
+
|
|
89
|
+
const execute = definition.execute;
|
|
90
|
+
definition.execute = async (toolCallId, params, signal, onUpdate, ctx) =>
|
|
91
|
+
runWithToolCallTimeout(definition.name, timeoutMs, signal, (signal) =>
|
|
92
|
+
execute.call(definition, toolCallId, params, signal, onUpdate, ctx),
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
apply(session: ToolRegistry) {
|
|
98
|
+
for (const { name } of session.getAllTools()) {
|
|
99
|
+
const definition = session.getToolDefinition(name);
|
|
100
|
+
if (definition) wrap(definition);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# skillsconf
|
|
2
|
+
|
|
3
|
+
Per-skill visibility manager for pi, with **packages** (named groups) for mass enable/disable.
|
|
4
|
+
|
|
5
|
+
Disabled skills are stripped from the system prompt every turn (via `before_agent_start`),
|
|
6
|
+
so they cost zero context. Nothing is moved on disk — packages are just named lists
|
|
7
|
+
stored in settings.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
/skillsconf
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Keybindings
|
|
14
|
+
|
|
15
|
+
- `↑` / `↓` or `j` / `k` — navigate; `Home`/`End` (`g`/`G`) jump
|
|
16
|
+
- `Space` — toggle the selected skill **or** the selected package (whole group on/off)
|
|
17
|
+
- `/` — fuzzy filter by name/description; `↑`/`↓` exit typing (filter kept) and
|
|
18
|
+
move; `←`/`→` edit the query; `Esc` clears, `Enter` keeps filter
|
|
19
|
+
- `e` — toggle grouped view (packages + ungrouped skills) vs flat list of all skills
|
|
20
|
+
- `n` — new package (type a name, `Enter` creates, `Esc` cancels)
|
|
21
|
+
- `a` — on a package: enter assign mode (cursor jumps to skills, `Space` toggles
|
|
22
|
+
membership, `Enter` done, `Esc` cancel without changing the package)
|
|
23
|
+
- `d` — delete what's under the cursor (package group, or skill files from disk)
|
|
24
|
+
with a `y`/`n` confirm (`Esc` cancels)
|
|
25
|
+
- `s` — save to settings, stay open (`● unsaved — press s to save` clears; takes
|
|
26
|
+
effect next turn)
|
|
27
|
+
- `Esc` / `q` — quit (`s` first if you want to keep changes); quitting with pending
|
|
28
|
+
changes shows `changes discarded (not saved)`
|
|
29
|
+
Package rows show `(enabled/total)`: `◉` all on, `◍` partial, `○` all off / empty.
|
|
30
|
+
Each package row leads with an accent `▸` marker and bold name so it stands out
|
|
31
|
+
from skills.
|
|
32
|
+
|
|
33
|
+
Skills that belong to a package are hidden from the main list and managed via
|
|
34
|
+
their package row (grouped view). `e` switches to a flat list showing every
|
|
35
|
+
skill; `/` search and assign mode always see every skill, with a `⟨package⟩`
|
|
36
|
+
tag showing membership.
|
|
37
|
+
|
|
38
|
+
## Persistence
|
|
39
|
+
|
|
40
|
+
Writes `disabledSkills` and `skillPackages` in `~/.pi/agent/settings.json`:
|
|
41
|
+
|
|
42
|
+
- Nothing disabled + no packages → both keys removed to keep settings clean
|
|
43
|
+
- `skillPackages` maps package name → sorted member skill names (empty packages kept)
|
|
44
|
+
- Stale names (skills no longer on disk) are filtered on the next save
|
|
45
|
+
|
|
46
|
+
No reload happens on save — the prompt filter re-reads settings from disk every
|
|
47
|
+
turn, so changes apply from the next agent turn on. Physically deleted skills
|
|
48
|
+
vanish from the loaded list after `/reload`.
|
|
49
|
+
|
|
50
|
+
## Keyboard shortcut
|
|
51
|
+
|
|
52
|
+
`Ctrl+Alt+S` opens skillsconf when supported by the host.
|
|
53
|
+
|
|
54
|
+
## How filtering works
|
|
55
|
+
|
|
56
|
+
pi renders loaded skills as `<skill><name>…` blocks inside `<available_skills>`.
|
|
57
|
+
The extension removes the blocks whose `<name>` is in `disabledSkills` (verbatim block
|
|
58
|
+
surgery — kept blocks are byte-identical). When all skills are disabled, the whole
|
|
59
|
+
section including its intro is removed.
|
|
60
|
+
|
|
61
|
+
## Development
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx tsc --noEmit --project ./tsconfig.json
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Tests are plain `node:assert` files (same style as `modelconf`):
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npx tsc --project ./tsconfig.json --outDir /tmp/skillsconf-test
|
|
71
|
+
node --test /tmp/skillsconf-test/src/filter.test.js \
|
|
72
|
+
/tmp/skillsconf-test/src/persistence.test.js \
|
|
73
|
+
/tmp/skillsconf-test/src/ui/SkillConfView.test.js
|
|
74
|
+
```
|