open-multi-agent-kit 0.96.1 → 0.96.2
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/CHANGELOG.md +41 -0
- package/README.md +12 -2
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/commands/resource-doctor-cli.d.ts +22 -0
- package/dist/commands/resource-doctor-cli.d.ts.map +1 -0
- package/dist/commands/resource-doctor-cli.js +98 -0
- package/dist/commands/resource-doctor-cli.js.map +1 -0
- package/dist/core/agent-session.d.ts +37 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +289 -5
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/completion-sound.d.ts +109 -0
- package/dist/core/completion-sound.d.ts.map +1 -0
- package/dist/core/completion-sound.js +165 -0
- package/dist/core/completion-sound.js.map +1 -0
- package/dist/core/context-budget-system-prompt-items.d.ts.map +1 -1
- package/dist/core/context-budget-system-prompt-items.js +2 -2
- package/dist/core/context-budget-system-prompt-items.js.map +1 -1
- package/dist/core/host-resource-probe.d.ts +63 -0
- package/dist/core/host-resource-probe.d.ts.map +1 -0
- package/dist/core/host-resource-probe.js +126 -0
- package/dist/core/host-resource-probe.js.map +1 -0
- package/dist/core/host-resource-snapshot.d.ts +54 -0
- package/dist/core/host-resource-snapshot.d.ts.map +1 -0
- package/dist/core/host-resource-snapshot.js +139 -0
- package/dist/core/host-resource-snapshot.js.map +1 -0
- package/dist/core/prompt-settlement.d.ts +81 -0
- package/dist/core/prompt-settlement.d.ts.map +1 -0
- package/dist/core/prompt-settlement.js +84 -0
- package/dist/core/prompt-settlement.js.map +1 -0
- package/dist/core/provider-resilience.d.ts.map +1 -1
- package/dist/core/provider-resilience.js +1 -1
- package/dist/core/provider-resilience.js.map +1 -1
- package/dist/core/provider-retry.d.ts +7 -0
- package/dist/core/provider-retry.d.ts.map +1 -1
- package/dist/core/provider-retry.js +24 -0
- package/dist/core/provider-retry.js.map +1 -1
- package/dist/core/provider-usage.d.ts +15 -2
- package/dist/core/provider-usage.d.ts.map +1 -1
- package/dist/core/provider-usage.js +110 -2
- package/dist/core/provider-usage.js.map +1 -1
- package/dist/core/reasoning-router-resolver.d.ts.map +1 -1
- package/dist/core/reasoning-router-resolver.js +1 -1
- package/dist/core/reasoning-router-resolver.js.map +1 -1
- package/dist/core/resource-admission-config.d.ts +55 -0
- package/dist/core/resource-admission-config.d.ts.map +1 -0
- package/dist/core/resource-admission-config.js +97 -0
- package/dist/core/resource-admission-config.js.map +1 -0
- package/dist/core/resource-admission.d.ts +81 -0
- package/dist/core/resource-admission.d.ts.map +1 -0
- package/dist/core/resource-admission.js +128 -0
- package/dist/core/resource-admission.js.map +1 -0
- package/dist/core/resource-governor-format.d.ts +50 -0
- package/dist/core/resource-governor-format.d.ts.map +1 -0
- package/dist/core/resource-governor-format.js +113 -0
- package/dist/core/resource-governor-format.js.map +1 -0
- package/dist/core/resource-governor-settings.d.ts +56 -0
- package/dist/core/resource-governor-settings.d.ts.map +1 -0
- package/dist/core/resource-governor-settings.js +89 -0
- package/dist/core/resource-governor-settings.js.map +1 -0
- package/dist/core/resource-observation-journal.d.ts +74 -0
- package/dist/core/resource-observation-journal.d.ts.map +1 -0
- package/dist/core/resource-observation-journal.js +198 -0
- package/dist/core/resource-observation-journal.js.map +1 -0
- package/dist/core/resource-safety-gate.d.ts +54 -0
- package/dist/core/resource-safety-gate.d.ts.map +1 -0
- package/dist/core/resource-safety-gate.js +51 -0
- package/dist/core/resource-safety-gate.js.map +1 -0
- package/dist/core/run-journal.d.ts.map +1 -1
- package/dist/core/run-journal.js +13 -1
- package/dist/core/run-journal.js.map +1 -1
- package/dist/core/run-resource-lease.d.ts +73 -0
- package/dist/core/run-resource-lease.d.ts.map +1 -0
- package/dist/core/run-resource-lease.js +78 -0
- package/dist/core/run-resource-lease.js.map +1 -0
- package/dist/core/session-bash-service.d.ts +12 -0
- package/dist/core/session-bash-service.d.ts.map +1 -1
- package/dist/core/session-bash-service.js +14 -0
- package/dist/core/session-bash-service.js.map +1 -1
- package/dist/core/session-system-prompt.d.ts +2 -0
- package/dist/core/session-system-prompt.d.ts.map +1 -1
- package/dist/core/session-system-prompt.js +2 -0
- package/dist/core/session-system-prompt.js.map +1 -1
- package/dist/core/session-termination.d.ts +7 -2
- package/dist/core/session-termination.d.ts.map +1 -1
- package/dist/core/session-termination.js +29 -1
- package/dist/core/session-termination.js.map +1 -1
- package/dist/core/settings-manager.d.ts +12 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +36 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/subagent-lane-launcher.d.ts +64 -0
- package/dist/core/subagent-lane-launcher.d.ts.map +1 -0
- package/dist/core/subagent-lane-launcher.js +107 -0
- package/dist/core/subagent-lane-launcher.js.map +1 -0
- package/dist/core/system-cpu-sampler.d.ts +45 -0
- package/dist/core/system-cpu-sampler.d.ts.map +1 -0
- package/dist/core/system-cpu-sampler.js +95 -0
- package/dist/core/system-cpu-sampler.js.map +1 -0
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +5 -8
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/workload-classifier.d.ts +32 -0
- package/dist/core/workload-classifier.d.ts.map +1 -0
- package/dist/core/workload-classifier.js +66 -0
- package/dist/core/workload-classifier.js.map +1 -0
- package/dist/core/workload-family-matcher.d.ts +3 -0
- package/dist/core/workload-family-matcher.d.ts.map +1 -0
- package/dist/core/workload-family-matcher.js +126 -0
- package/dist/core/workload-family-matcher.js.map +1 -0
- package/dist/core/workload-permit-pool.d.ts +78 -0
- package/dist/core/workload-permit-pool.d.ts.map +1 -0
- package/dist/core/workload-permit-pool.js +138 -0
- package/dist/core/workload-permit-pool.js.map +1 -0
- package/dist/core/workload-shard-executor.d.ts +68 -0
- package/dist/core/workload-shard-executor.d.ts.map +1 -0
- package/dist/core/workload-shard-executor.js +156 -0
- package/dist/core/workload-shard-executor.js.map +1 -0
- package/dist/core/workload-shard-plan.d.ts +81 -0
- package/dist/core/workload-shard-plan.d.ts.map +1 -0
- package/dist/core/workload-shard-plan.js +183 -0
- package/dist/core/workload-shard-plan.js.map +1 -0
- package/dist/core/workload-shard-runner.d.ts +11 -0
- package/dist/core/workload-shard-runner.d.ts.map +1 -0
- package/dist/core/workload-shard-runner.js +98 -0
- package/dist/core/workload-shard-runner.js.map +1 -0
- package/dist/core/workload-shard-store.d.ts +44 -0
- package/dist/core/workload-shard-store.d.ts.map +1 -0
- package/dist/core/workload-shard-store.js +189 -0
- package/dist/core/workload-shard-store.js.map +1 -0
- package/dist/core/workload-sharders.d.ts +47 -0
- package/dist/core/workload-sharders.d.ts.map +1 -0
- package/dist/core/workload-sharders.js +162 -0
- package/dist/core/workload-sharders.js.map +1 -0
- package/dist/core/workload-shell-scan.d.ts +12 -0
- package/dist/core/workload-shell-scan.d.ts.map +1 -0
- package/dist/core/workload-shell-scan.js +79 -0
- package/dist/core/workload-shell-scan.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +6 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +4 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +81 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/providers.md +1 -1
- package/docs/sdk.md +4 -0
- package/docs/settings.md +64 -2
- package/docs/skills.md +16 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/subagent/agent-capability-router.test.ts +7 -0
- package/examples/extensions/subagent/domain-profiles.ts +0 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +18 -18
- package/package.json +7 -9
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { PromptSettledEvent } from "./prompt-settlement.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Cross-platform completion sound service (OMK v0.97.x roadmap §17, M4/PR7).
|
|
4
|
+
*
|
|
5
|
+
* Consumes `prompt_settled` only (§16.1: never `agent_end`) and is a pure UX
|
|
6
|
+
* signal — it must never influence run outcome, block the event loop, or
|
|
7
|
+
* delay process exit (§17.4: spawn with `stdio: "ignore"`, `shell: false`,
|
|
8
|
+
* `unref()`, bounded kill timer, never awaited by the prompt path).
|
|
9
|
+
*
|
|
10
|
+
* Security (§22.1): every backend uses a fixed executable and fixed argv;
|
|
11
|
+
* no user text, paths, or model output is ever interpolated.
|
|
12
|
+
*
|
|
13
|
+
* Backend selection is a deterministic candidate chain per platform
|
|
14
|
+
* (§17.3); a spawn failure falls through to the next candidate and finally
|
|
15
|
+
* to the terminal BEL. All failures are diagnostics only (§4.6).
|
|
16
|
+
*/
|
|
17
|
+
export interface CompletionSoundSettings {
|
|
18
|
+
enabled?: boolean;
|
|
19
|
+
minDurationMs?: number;
|
|
20
|
+
onSuccess?: boolean;
|
|
21
|
+
onFailure?: boolean;
|
|
22
|
+
terminalBellFallback?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface NotificationSettings {
|
|
25
|
+
completionSound?: CompletionSoundSettings;
|
|
26
|
+
}
|
|
27
|
+
export type CompletionSoundBackend = "macos-afplay" | "windows-system-sound" | "linux-canberra" | "linux-paplay" | "linux-aplay" | "terminal-bell" | "none";
|
|
28
|
+
export interface CompletionSoundRequest {
|
|
29
|
+
readonly promptRunId: string;
|
|
30
|
+
readonly outcome: "completed" | "failed" | "aborted";
|
|
31
|
+
readonly durationMs: number;
|
|
32
|
+
}
|
|
33
|
+
export interface CompletionSoundResult {
|
|
34
|
+
readonly backend: CompletionSoundBackend;
|
|
35
|
+
readonly attempted: boolean;
|
|
36
|
+
readonly success: boolean;
|
|
37
|
+
readonly diagnostic?: string;
|
|
38
|
+
}
|
|
39
|
+
export declare const COMPLETION_SOUND_ENV = "OMK_COMPLETION_SOUND";
|
|
40
|
+
export interface ResolvedCompletionSoundSettings {
|
|
41
|
+
readonly enabled: boolean;
|
|
42
|
+
readonly minDurationMs: number;
|
|
43
|
+
readonly onSuccess: boolean;
|
|
44
|
+
readonly onFailure: boolean;
|
|
45
|
+
readonly terminalBellFallback: boolean;
|
|
46
|
+
}
|
|
47
|
+
/** Defaults per §17.1; `OMK_COMPLETION_SOUND=0|1` (§18.2) overrides `enabled`. */
|
|
48
|
+
export declare function resolveCompletionSoundSettings(settings?: CompletionSoundSettings, env?: NodeJS.ProcessEnv): ResolvedCompletionSoundSettings;
|
|
49
|
+
export interface CompletionSoundSurface {
|
|
50
|
+
/** Only the interactive TUI plays sounds by default (§17.4). */
|
|
51
|
+
readonly isTui: boolean;
|
|
52
|
+
readonly isTty: boolean;
|
|
53
|
+
readonly isCi: boolean;
|
|
54
|
+
}
|
|
55
|
+
/** §17.1/§17.4 gate: settings, outcome mapping, duration floor, and surface. */
|
|
56
|
+
export declare function shouldPlayCompletionSound(input: {
|
|
57
|
+
readonly settings: ResolvedCompletionSoundSettings;
|
|
58
|
+
readonly event: PromptSettledEvent;
|
|
59
|
+
readonly surface: CompletionSoundSurface;
|
|
60
|
+
}): boolean;
|
|
61
|
+
export interface CompletionSoundCandidate {
|
|
62
|
+
readonly backend: CompletionSoundBackend;
|
|
63
|
+
/** Fixed argv (§22.1); empty for terminal-bell (written, not spawned). */
|
|
64
|
+
readonly argv: readonly string[];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Deterministic backend candidate chain per platform (§17.3, §23.2 property
|
|
68
|
+
* 14). The runner tries candidates in order on spawn failure.
|
|
69
|
+
*/
|
|
70
|
+
export declare function selectCompletionSoundCandidates(input: {
|
|
71
|
+
readonly platform: NodeJS.Platform;
|
|
72
|
+
readonly isWsl: boolean;
|
|
73
|
+
readonly terminalBellFallback: boolean;
|
|
74
|
+
}): readonly CompletionSoundCandidate[];
|
|
75
|
+
export interface CompletionSoundIo {
|
|
76
|
+
/** Spawn a fixed argv; resolve success/failure. Must never throw synchronously into the caller. */
|
|
77
|
+
readonly spawnBackend: (argv: readonly string[]) => Promise<{
|
|
78
|
+
readonly ok: boolean;
|
|
79
|
+
readonly diagnostic?: string;
|
|
80
|
+
}>;
|
|
81
|
+
readonly writeBell: () => boolean;
|
|
82
|
+
}
|
|
83
|
+
/** Default IO: detached-ish fire-and-forget spawn per §17.4. */
|
|
84
|
+
export declare function defaultCompletionSoundIo(): CompletionSoundIo;
|
|
85
|
+
/**
|
|
86
|
+
* Stateful service: once per promptRunId (§17.4), deterministic candidate
|
|
87
|
+
* walk, all failures downgraded to diagnostics. `handleSettled` is fire and
|
|
88
|
+
* forget — callers must not await it on the prompt path.
|
|
89
|
+
*/
|
|
90
|
+
export interface CompletionSoundServiceDeps {
|
|
91
|
+
readonly getSettings: () => CompletionSoundSettings | undefined;
|
|
92
|
+
readonly surface: () => CompletionSoundSurface;
|
|
93
|
+
readonly candidates: () => readonly CompletionSoundCandidate[];
|
|
94
|
+
readonly io?: CompletionSoundIo;
|
|
95
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
96
|
+
/** §20.2 observability hook; fail-open, never affects the result. */
|
|
97
|
+
readonly onResult?: (result: CompletionSoundResult) => void;
|
|
98
|
+
}
|
|
99
|
+
export declare class CompletionSoundService {
|
|
100
|
+
private readonly playedRunIds;
|
|
101
|
+
private lastResult;
|
|
102
|
+
private readonly deps;
|
|
103
|
+
constructor(deps: CompletionSoundServiceDeps);
|
|
104
|
+
/** Most recent attempt, for diagnostics/tests. */
|
|
105
|
+
get lastPlaybackResult(): CompletionSoundResult | null;
|
|
106
|
+
handleSettled(event: PromptSettledEvent): Promise<CompletionSoundResult>;
|
|
107
|
+
private finish;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=completion-sound.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"completion-sound.d.ts","sourceRoot":"","sources":["../../src/core/completion-sound.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,uBAAuB;IACvC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB;IACpC,eAAe,CAAC,EAAE,uBAAuB,CAAC;CAC1C;AAED,MAAM,MAAM,sBAAsB,GAC/B,cAAc,GACd,sBAAsB,GACtB,gBAAgB,GAChB,cAAc,GACd,aAAa,GACb,eAAe,GACf,MAAM,CAAC;AAEV,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;IACrD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,OAAO,EAAE,sBAAsB,CAAC;IACzC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,eAAO,MAAM,oBAAoB,yBAAyB,CAAC;AAG3D,MAAM,WAAW,+BAA+B;IAC/C,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAC;CACvC;AAED,oFAAkF;AAClF,wBAAgB,8BAA8B,CAC7C,QAAQ,CAAC,EAAE,uBAAuB,EAClC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAClC,+BAA+B,CAgBjC;AAED,MAAM,WAAW,sBAAsB;IACtC,iEAAgE;IAChE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACvB;AAED,kFAAgF;AAChF,wBAAgB,yBAAyB,CAAC,KAAK,EAAE;IAChD,QAAQ,CAAC,QAAQ,EAAE,+BAA+B,CAAC;IACnD,QAAQ,CAAC,KAAK,EAAE,kBAAkB,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,sBAAsB,CAAC;CACzC,GAAG,OAAO,CAgBV;AAED,MAAM,WAAW,wBAAwB;IACxC,QAAQ,CAAC,OAAO,EAAE,sBAAsB,CAAC;IACzC,2EAA0E;IAC1E,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CACjC;AAED;;;GAGG;AACH,wBAAgB,+BAA+B,CAAC,KAAK,EAAE;IACtD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAC;CACvC,GAAG,SAAS,wBAAwB,EAAE,CAqBtC;AAOD,MAAM,WAAW,iBAAiB;IACjC,mGAAmG;IACnG,QAAQ,CAAC,YAAY,EAAE,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,OAAO,CAAC;QAAE,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpH,QAAQ,CAAC,SAAS,EAAE,MAAM,OAAO,CAAC;CAClC;AAED,iEAAgE;AAChE,wBAAgB,wBAAwB,IAAI,iBAAiB,CAoC5D;AAED;;;;GAIG;AACH,MAAM,WAAW,0BAA0B;IAC1C,QAAQ,CAAC,WAAW,EAAE,MAAM,uBAAuB,GAAG,SAAS,CAAC;IAChE,QAAQ,CAAC,OAAO,EAAE,MAAM,sBAAsB,CAAC;IAC/C,QAAQ,CAAC,UAAU,EAAE,MAAM,SAAS,wBAAwB,EAAE,CAAC;IAC/D,QAAQ,CAAC,EAAE,CAAC,EAAE,iBAAiB,CAAC;IAChC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC,sEAAqE;IACrE,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,qBAAqB,KAAK,IAAI,CAAC;CAC5D;AAED,qBAAa,sBAAsB;IAClC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAClD,OAAO,CAAC,UAAU,CAAsC;IACxD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA6B;IAElD,YAAY,IAAI,EAAE,0BAA0B,EAE3C;IAED,kDAAkD;IAClD,IAAI,kBAAkB,IAAI,qBAAqB,GAAG,IAAI,CAErD;IAEK,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAiC7E;IAED,OAAO,CAAC,MAAM;CASd","sourcesContent":["import { spawn } from \"node:child_process\";\nimport type { PromptSettledEvent } from \"./prompt-settlement.ts\";\n\n/**\n * Cross-platform completion sound service (OMK v0.97.x roadmap §17, M4/PR7).\n *\n * Consumes `prompt_settled` only (§16.1: never `agent_end`) and is a pure UX\n * signal — it must never influence run outcome, block the event loop, or\n * delay process exit (§17.4: spawn with `stdio: \"ignore\"`, `shell: false`,\n * `unref()`, bounded kill timer, never awaited by the prompt path).\n *\n * Security (§22.1): every backend uses a fixed executable and fixed argv;\n * no user text, paths, or model output is ever interpolated.\n *\n * Backend selection is a deterministic candidate chain per platform\n * (§17.3); a spawn failure falls through to the next candidate and finally\n * to the terminal BEL. All failures are diagnostics only (§4.6).\n */\n\nexport interface CompletionSoundSettings {\n\tenabled?: boolean; // default: false (§17.1: backward compat, CI/headless surprise, accessibility)\n\tminDurationMs?: number; // default: 5000\n\tonSuccess?: boolean; // default: true\n\tonFailure?: boolean; // default: true\n\tterminalBellFallback?: boolean; // default: true\n}\n\nexport interface NotificationSettings {\n\tcompletionSound?: CompletionSoundSettings;\n}\n\nexport type CompletionSoundBackend =\n\t| \"macos-afplay\"\n\t| \"windows-system-sound\"\n\t| \"linux-canberra\"\n\t| \"linux-paplay\"\n\t| \"linux-aplay\"\n\t| \"terminal-bell\"\n\t| \"none\";\n\nexport interface CompletionSoundRequest {\n\treadonly promptRunId: string;\n\treadonly outcome: \"completed\" | \"failed\" | \"aborted\";\n\treadonly durationMs: number;\n}\n\nexport interface CompletionSoundResult {\n\treadonly backend: CompletionSoundBackend;\n\treadonly attempted: boolean;\n\treadonly success: boolean;\n\treadonly diagnostic?: string;\n}\n\nexport const COMPLETION_SOUND_ENV = \"OMK_COMPLETION_SOUND\";\nconst SPAWN_KILL_TIMEOUT_MS = 1000;\n\nexport interface ResolvedCompletionSoundSettings {\n\treadonly enabled: boolean;\n\treadonly minDurationMs: number;\n\treadonly onSuccess: boolean;\n\treadonly onFailure: boolean;\n\treadonly terminalBellFallback: boolean;\n}\n\n/** Defaults per §17.1; `OMK_COMPLETION_SOUND=0|1` (§18.2) overrides `enabled`. */\nexport function resolveCompletionSoundSettings(\n\tsettings?: CompletionSoundSettings,\n\tenv: NodeJS.ProcessEnv = process.env,\n): ResolvedCompletionSoundSettings {\n\tlet enabled = settings?.enabled ?? false;\n\tconst envValue = env[COMPLETION_SOUND_ENV]?.trim();\n\tif (envValue === \"0\") {\n\t\tenabled = false;\n\t} else if (envValue === \"1\") {\n\t\tenabled = true;\n\t}\n\treturn {\n\t\tenabled,\n\t\tminDurationMs:\n\t\t\ttypeof settings?.minDurationMs === \"number\" && settings.minDurationMs >= 0 ? settings.minDurationMs : 5000,\n\t\tonSuccess: settings?.onSuccess ?? true,\n\t\tonFailure: settings?.onFailure ?? true,\n\t\tterminalBellFallback: settings?.terminalBellFallback ?? true,\n\t};\n}\n\nexport interface CompletionSoundSurface {\n\t/** Only the interactive TUI plays sounds by default (§17.4). */\n\treadonly isTui: boolean;\n\treadonly isTty: boolean;\n\treadonly isCi: boolean;\n}\n\n/** §17.1/§17.4 gate: settings, outcome mapping, duration floor, and surface. */\nexport function shouldPlayCompletionSound(input: {\n\treadonly settings: ResolvedCompletionSoundSettings;\n\treadonly event: PromptSettledEvent;\n\treadonly surface: CompletionSoundSurface;\n}): boolean {\n\tconst { settings, event, surface } = input;\n\tif (!settings.enabled || !surface.isTui || !surface.isTty || surface.isCi) {\n\t\treturn false;\n\t}\n\tif (event.durationMs < settings.minDurationMs) {\n\t\treturn false;\n\t}\n\tif (event.outcome === \"completed\") {\n\t\treturn settings.onSuccess;\n\t}\n\tif (event.outcome === \"failed\") {\n\t\treturn settings.onFailure;\n\t}\n\t// User-initiated aborts never chime: the user is already at the keyboard.\n\treturn false;\n}\n\nexport interface CompletionSoundCandidate {\n\treadonly backend: CompletionSoundBackend;\n\t/** Fixed argv (§22.1); empty for terminal-bell (written, not spawned). */\n\treadonly argv: readonly string[];\n}\n\n/**\n * Deterministic backend candidate chain per platform (§17.3, §23.2 property\n * 14). The runner tries candidates in order on spawn failure.\n */\nexport function selectCompletionSoundCandidates(input: {\n\treadonly platform: NodeJS.Platform;\n\treadonly isWsl: boolean;\n\treadonly terminalBellFallback: boolean;\n}): readonly CompletionSoundCandidate[] {\n\tconst bell: CompletionSoundCandidate[] = input.terminalBellFallback ? [{ backend: \"terminal-bell\", argv: [] }] : [];\n\tif (input.platform === \"darwin\") {\n\t\treturn [{ backend: \"macos-afplay\", argv: [\"afplay\", \"/System/Library/Sounds/Glass.aiff\"] }, ...bell];\n\t}\n\tif (input.platform === \"win32\") {\n\t\treturn [{ backend: \"windows-system-sound\", argv: windowsSystemSoundArgv(\"powershell.exe\") }, ...bell];\n\t}\n\tif (input.platform === \"linux\" && input.isWsl) {\n\t\t// §17.3 WSL: Windows PowerShell first, then the terminal BEL.\n\t\treturn [{ backend: \"windows-system-sound\", argv: windowsSystemSoundArgv(\"powershell.exe\") }, ...bell];\n\t}\n\tif (input.platform === \"linux\") {\n\t\treturn [\n\t\t\t{ backend: \"linux-canberra\", argv: [\"canberra-gtk-play\", \"-i\", \"complete\"] },\n\t\t\t{ backend: \"linux-paplay\", argv: [\"paplay\", \"/usr/share/sounds/freedesktop/stereo/complete.oga\"] },\n\t\t\t{ backend: \"linux-aplay\", argv: [\"aplay\", \"-q\", \"/usr/share/sounds/alsa/Front_Center.wav\"] },\n\t\t\t...bell,\n\t\t];\n\t}\n\treturn [...bell];\n}\n\n/** Fixed expression only (§17.3 Windows): no user text ever enters the command. */\nfunction windowsSystemSoundArgv(executable: string): readonly string[] {\n\treturn [executable, \"-NoProfile\", \"-NonInteractive\", \"-Command\", \"[System.Media.SystemSounds]::Asterisk.Play()\"];\n}\n\nexport interface CompletionSoundIo {\n\t/** Spawn a fixed argv; resolve success/failure. Must never throw synchronously into the caller. */\n\treadonly spawnBackend: (argv: readonly string[]) => Promise<{ readonly ok: boolean; readonly diagnostic?: string }>;\n\treadonly writeBell: () => boolean;\n}\n\n/** Default IO: detached-ish fire-and-forget spawn per §17.4. */\nexport function defaultCompletionSoundIo(): CompletionSoundIo {\n\treturn {\n\t\tspawnBackend: (argv) =>\n\t\t\tnew Promise((resolve) => {\n\t\t\t\ttry {\n\t\t\t\t\tconst [executable, ...args] = argv;\n\t\t\t\t\tconst child = spawn(executable, args, { stdio: \"ignore\", shell: false, windowsHide: true });\n\t\t\t\t\tconst killTimer = setTimeout(() => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tchild.kill();\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t// Best effort only.\n\t\t\t\t\t\t}\n\t\t\t\t\t}, SPAWN_KILL_TIMEOUT_MS);\n\t\t\t\t\tkillTimer.unref();\n\t\t\t\t\tchild.once(\"error\", (error: NodeJS.ErrnoException) =>\n\t\t\t\t\t\tresolve({ ok: false, diagnostic: error.code ?? \"spawn_error\" }),\n\t\t\t\t\t);\n\t\t\t\t\t// Success means \"spawned\"; playback is not awaited (§17.4).\n\t\t\t\t\tchild.once(\"spawn\", () => {\n\t\t\t\t\t\tchild.unref();\n\t\t\t\t\t\tresolve({ ok: true });\n\t\t\t\t\t});\n\t\t\t\t} catch {\n\t\t\t\t\tresolve({ ok: false, diagnostic: \"spawn_error\" });\n\t\t\t\t}\n\t\t\t}),\n\t\twriteBell: () => {\n\t\t\ttry {\n\t\t\t\tprocess.stdout.write(\"\\u0007\");\n\t\t\t\treturn true;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t},\n\t};\n}\n\n/**\n * Stateful service: once per promptRunId (§17.4), deterministic candidate\n * walk, all failures downgraded to diagnostics. `handleSettled` is fire and\n * forget — callers must not await it on the prompt path.\n */\nexport interface CompletionSoundServiceDeps {\n\treadonly getSettings: () => CompletionSoundSettings | undefined;\n\treadonly surface: () => CompletionSoundSurface;\n\treadonly candidates: () => readonly CompletionSoundCandidate[];\n\treadonly io?: CompletionSoundIo;\n\treadonly env?: NodeJS.ProcessEnv;\n\t/** §20.2 observability hook; fail-open, never affects the result. */\n\treadonly onResult?: (result: CompletionSoundResult) => void;\n}\n\nexport class CompletionSoundService {\n\tprivate readonly playedRunIds = new Set<string>();\n\tprivate lastResult: CompletionSoundResult | null = null;\n\tprivate readonly deps: CompletionSoundServiceDeps;\n\n\tconstructor(deps: CompletionSoundServiceDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\t/** Most recent attempt, for diagnostics/tests. */\n\tget lastPlaybackResult(): CompletionSoundResult | null {\n\t\treturn this.lastResult;\n\t}\n\n\tasync handleSettled(event: PromptSettledEvent): Promise<CompletionSoundResult> {\n\t\ttry {\n\t\t\tconst settings = resolveCompletionSoundSettings(this.deps.getSettings(), this.deps.env ?? process.env);\n\t\t\tif (this.playedRunIds.has(event.promptRunId)) {\n\t\t\t\treturn this.finish({ backend: \"none\", attempted: false, success: false, diagnostic: \"already_played\" });\n\t\t\t}\n\t\t\tif (!shouldPlayCompletionSound({ settings, event, surface: this.deps.surface() })) {\n\t\t\t\treturn this.finish({ backend: \"none\", attempted: false, success: true });\n\t\t\t}\n\t\t\tthis.playedRunIds.add(event.promptRunId);\n\t\t\tconst io = this.deps.io ?? defaultCompletionSoundIo();\n\t\t\tconst diagnostics: string[] = [];\n\t\t\tfor (const candidate of this.deps.candidates()) {\n\t\t\t\tif (candidate.backend === \"terminal-bell\") {\n\t\t\t\t\tconst ok = io.writeBell();\n\t\t\t\t\treturn this.finish({\n\t\t\t\t\t\tbackend: \"terminal-bell\",\n\t\t\t\t\t\tattempted: true,\n\t\t\t\t\t\tsuccess: ok,\n\t\t\t\t\t\tdiagnostic: diagnostics.length > 0 ? diagnostics.join(\", \") : undefined,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tconst spawned = await io.spawnBackend(candidate.argv);\n\t\t\t\tif (spawned.ok) {\n\t\t\t\t\treturn this.finish({ backend: candidate.backend, attempted: true, success: true });\n\t\t\t\t}\n\t\t\t\tdiagnostics.push(`${candidate.backend}: ${spawned.diagnostic ?? \"failed\"}`);\n\t\t\t}\n\t\t\treturn this.finish({ backend: \"none\", attempted: true, success: false, diagnostic: diagnostics.join(\", \") });\n\t\t} catch (error) {\n\t\t\t// §23.2 property 13: sound failure never changes the prompt outcome.\n\t\t\treturn this.finish({ backend: \"none\", attempted: true, success: false, diagnostic: String(error) });\n\t\t}\n\t}\n\n\tprivate finish(result: CompletionSoundResult): CompletionSoundResult {\n\t\tthis.lastResult = result;\n\t\ttry {\n\t\t\tthis.deps.onResult?.(result);\n\t\t} catch {\n\t\t\t// Observability must never affect the sound path.\n\t\t}\n\t\treturn result;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export const COMPLETION_SOUND_ENV = "OMK_COMPLETION_SOUND";
|
|
3
|
+
const SPAWN_KILL_TIMEOUT_MS = 1000;
|
|
4
|
+
/** Defaults per §17.1; `OMK_COMPLETION_SOUND=0|1` (§18.2) overrides `enabled`. */
|
|
5
|
+
export function resolveCompletionSoundSettings(settings, env = process.env) {
|
|
6
|
+
let enabled = settings?.enabled ?? false;
|
|
7
|
+
const envValue = env[COMPLETION_SOUND_ENV]?.trim();
|
|
8
|
+
if (envValue === "0") {
|
|
9
|
+
enabled = false;
|
|
10
|
+
}
|
|
11
|
+
else if (envValue === "1") {
|
|
12
|
+
enabled = true;
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
enabled,
|
|
16
|
+
minDurationMs: typeof settings?.minDurationMs === "number" && settings.minDurationMs >= 0 ? settings.minDurationMs : 5000,
|
|
17
|
+
onSuccess: settings?.onSuccess ?? true,
|
|
18
|
+
onFailure: settings?.onFailure ?? true,
|
|
19
|
+
terminalBellFallback: settings?.terminalBellFallback ?? true,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** §17.1/§17.4 gate: settings, outcome mapping, duration floor, and surface. */
|
|
23
|
+
export function shouldPlayCompletionSound(input) {
|
|
24
|
+
const { settings, event, surface } = input;
|
|
25
|
+
if (!settings.enabled || !surface.isTui || !surface.isTty || surface.isCi) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
if (event.durationMs < settings.minDurationMs) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if (event.outcome === "completed") {
|
|
32
|
+
return settings.onSuccess;
|
|
33
|
+
}
|
|
34
|
+
if (event.outcome === "failed") {
|
|
35
|
+
return settings.onFailure;
|
|
36
|
+
}
|
|
37
|
+
// User-initiated aborts never chime: the user is already at the keyboard.
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Deterministic backend candidate chain per platform (§17.3, §23.2 property
|
|
42
|
+
* 14). The runner tries candidates in order on spawn failure.
|
|
43
|
+
*/
|
|
44
|
+
export function selectCompletionSoundCandidates(input) {
|
|
45
|
+
const bell = input.terminalBellFallback ? [{ backend: "terminal-bell", argv: [] }] : [];
|
|
46
|
+
if (input.platform === "darwin") {
|
|
47
|
+
return [{ backend: "macos-afplay", argv: ["afplay", "/System/Library/Sounds/Glass.aiff"] }, ...bell];
|
|
48
|
+
}
|
|
49
|
+
if (input.platform === "win32") {
|
|
50
|
+
return [{ backend: "windows-system-sound", argv: windowsSystemSoundArgv("powershell.exe") }, ...bell];
|
|
51
|
+
}
|
|
52
|
+
if (input.platform === "linux" && input.isWsl) {
|
|
53
|
+
// §17.3 WSL: Windows PowerShell first, then the terminal BEL.
|
|
54
|
+
return [{ backend: "windows-system-sound", argv: windowsSystemSoundArgv("powershell.exe") }, ...bell];
|
|
55
|
+
}
|
|
56
|
+
if (input.platform === "linux") {
|
|
57
|
+
return [
|
|
58
|
+
{ backend: "linux-canberra", argv: ["canberra-gtk-play", "-i", "complete"] },
|
|
59
|
+
{ backend: "linux-paplay", argv: ["paplay", "/usr/share/sounds/freedesktop/stereo/complete.oga"] },
|
|
60
|
+
{ backend: "linux-aplay", argv: ["aplay", "-q", "/usr/share/sounds/alsa/Front_Center.wav"] },
|
|
61
|
+
...bell,
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
return [...bell];
|
|
65
|
+
}
|
|
66
|
+
/** Fixed expression only (§17.3 Windows): no user text ever enters the command. */
|
|
67
|
+
function windowsSystemSoundArgv(executable) {
|
|
68
|
+
return [executable, "-NoProfile", "-NonInteractive", "-Command", "[System.Media.SystemSounds]::Asterisk.Play()"];
|
|
69
|
+
}
|
|
70
|
+
/** Default IO: detached-ish fire-and-forget spawn per §17.4. */
|
|
71
|
+
export function defaultCompletionSoundIo() {
|
|
72
|
+
return {
|
|
73
|
+
spawnBackend: (argv) => new Promise((resolve) => {
|
|
74
|
+
try {
|
|
75
|
+
const [executable, ...args] = argv;
|
|
76
|
+
const child = spawn(executable, args, { stdio: "ignore", shell: false, windowsHide: true });
|
|
77
|
+
const killTimer = setTimeout(() => {
|
|
78
|
+
try {
|
|
79
|
+
child.kill();
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Best effort only.
|
|
83
|
+
}
|
|
84
|
+
}, SPAWN_KILL_TIMEOUT_MS);
|
|
85
|
+
killTimer.unref();
|
|
86
|
+
child.once("error", (error) => resolve({ ok: false, diagnostic: error.code ?? "spawn_error" }));
|
|
87
|
+
// Success means "spawned"; playback is not awaited (§17.4).
|
|
88
|
+
child.once("spawn", () => {
|
|
89
|
+
child.unref();
|
|
90
|
+
resolve({ ok: true });
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
resolve({ ok: false, diagnostic: "spawn_error" });
|
|
95
|
+
}
|
|
96
|
+
}),
|
|
97
|
+
writeBell: () => {
|
|
98
|
+
try {
|
|
99
|
+
process.stdout.write("\u0007");
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export class CompletionSoundService {
|
|
109
|
+
playedRunIds = new Set();
|
|
110
|
+
lastResult = null;
|
|
111
|
+
deps;
|
|
112
|
+
constructor(deps) {
|
|
113
|
+
this.deps = deps;
|
|
114
|
+
}
|
|
115
|
+
/** Most recent attempt, for diagnostics/tests. */
|
|
116
|
+
get lastPlaybackResult() {
|
|
117
|
+
return this.lastResult;
|
|
118
|
+
}
|
|
119
|
+
async handleSettled(event) {
|
|
120
|
+
try {
|
|
121
|
+
const settings = resolveCompletionSoundSettings(this.deps.getSettings(), this.deps.env ?? process.env);
|
|
122
|
+
if (this.playedRunIds.has(event.promptRunId)) {
|
|
123
|
+
return this.finish({ backend: "none", attempted: false, success: false, diagnostic: "already_played" });
|
|
124
|
+
}
|
|
125
|
+
if (!shouldPlayCompletionSound({ settings, event, surface: this.deps.surface() })) {
|
|
126
|
+
return this.finish({ backend: "none", attempted: false, success: true });
|
|
127
|
+
}
|
|
128
|
+
this.playedRunIds.add(event.promptRunId);
|
|
129
|
+
const io = this.deps.io ?? defaultCompletionSoundIo();
|
|
130
|
+
const diagnostics = [];
|
|
131
|
+
for (const candidate of this.deps.candidates()) {
|
|
132
|
+
if (candidate.backend === "terminal-bell") {
|
|
133
|
+
const ok = io.writeBell();
|
|
134
|
+
return this.finish({
|
|
135
|
+
backend: "terminal-bell",
|
|
136
|
+
attempted: true,
|
|
137
|
+
success: ok,
|
|
138
|
+
diagnostic: diagnostics.length > 0 ? diagnostics.join(", ") : undefined,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const spawned = await io.spawnBackend(candidate.argv);
|
|
142
|
+
if (spawned.ok) {
|
|
143
|
+
return this.finish({ backend: candidate.backend, attempted: true, success: true });
|
|
144
|
+
}
|
|
145
|
+
diagnostics.push(`${candidate.backend}: ${spawned.diagnostic ?? "failed"}`);
|
|
146
|
+
}
|
|
147
|
+
return this.finish({ backend: "none", attempted: true, success: false, diagnostic: diagnostics.join(", ") });
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
// §23.2 property 13: sound failure never changes the prompt outcome.
|
|
151
|
+
return this.finish({ backend: "none", attempted: true, success: false, diagnostic: String(error) });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
finish(result) {
|
|
155
|
+
this.lastResult = result;
|
|
156
|
+
try {
|
|
157
|
+
this.deps.onResult?.(result);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// Observability must never affect the sound path.
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=completion-sound.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"completion-sound.js","sourceRoot":"","sources":["../../src/core/completion-sound.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"node:child_process\";\nimport type { PromptSettledEvent } from \"./prompt-settlement.ts\";\n\n/**\n * Cross-platform completion sound service (OMK v0.97.x roadmap §17, M4/PR7).\n *\n * Consumes `prompt_settled` only (§16.1: never `agent_end`) and is a pure UX\n * signal — it must never influence run outcome, block the event loop, or\n * delay process exit (§17.4: spawn with `stdio: \"ignore\"`, `shell: false`,\n * `unref()`, bounded kill timer, never awaited by the prompt path).\n *\n * Security (§22.1): every backend uses a fixed executable and fixed argv;\n * no user text, paths, or model output is ever interpolated.\n *\n * Backend selection is a deterministic candidate chain per platform\n * (§17.3); a spawn failure falls through to the next candidate and finally\n * to the terminal BEL. All failures are diagnostics only (§4.6).\n */\n\nexport interface CompletionSoundSettings {\n\tenabled?: boolean; // default: false (§17.1: backward compat, CI/headless surprise, accessibility)\n\tminDurationMs?: number; // default: 5000\n\tonSuccess?: boolean; // default: true\n\tonFailure?: boolean; // default: true\n\tterminalBellFallback?: boolean; // default: true\n}\n\nexport interface NotificationSettings {\n\tcompletionSound?: CompletionSoundSettings;\n}\n\nexport type CompletionSoundBackend =\n\t| \"macos-afplay\"\n\t| \"windows-system-sound\"\n\t| \"linux-canberra\"\n\t| \"linux-paplay\"\n\t| \"linux-aplay\"\n\t| \"terminal-bell\"\n\t| \"none\";\n\nexport interface CompletionSoundRequest {\n\treadonly promptRunId: string;\n\treadonly outcome: \"completed\" | \"failed\" | \"aborted\";\n\treadonly durationMs: number;\n}\n\nexport interface CompletionSoundResult {\n\treadonly backend: CompletionSoundBackend;\n\treadonly attempted: boolean;\n\treadonly success: boolean;\n\treadonly diagnostic?: string;\n}\n\nexport const COMPLETION_SOUND_ENV = \"OMK_COMPLETION_SOUND\";\nconst SPAWN_KILL_TIMEOUT_MS = 1000;\n\nexport interface ResolvedCompletionSoundSettings {\n\treadonly enabled: boolean;\n\treadonly minDurationMs: number;\n\treadonly onSuccess: boolean;\n\treadonly onFailure: boolean;\n\treadonly terminalBellFallback: boolean;\n}\n\n/** Defaults per §17.1; `OMK_COMPLETION_SOUND=0|1` (§18.2) overrides `enabled`. */\nexport function resolveCompletionSoundSettings(\n\tsettings?: CompletionSoundSettings,\n\tenv: NodeJS.ProcessEnv = process.env,\n): ResolvedCompletionSoundSettings {\n\tlet enabled = settings?.enabled ?? false;\n\tconst envValue = env[COMPLETION_SOUND_ENV]?.trim();\n\tif (envValue === \"0\") {\n\t\tenabled = false;\n\t} else if (envValue === \"1\") {\n\t\tenabled = true;\n\t}\n\treturn {\n\t\tenabled,\n\t\tminDurationMs:\n\t\t\ttypeof settings?.minDurationMs === \"number\" && settings.minDurationMs >= 0 ? settings.minDurationMs : 5000,\n\t\tonSuccess: settings?.onSuccess ?? true,\n\t\tonFailure: settings?.onFailure ?? true,\n\t\tterminalBellFallback: settings?.terminalBellFallback ?? true,\n\t};\n}\n\nexport interface CompletionSoundSurface {\n\t/** Only the interactive TUI plays sounds by default (§17.4). */\n\treadonly isTui: boolean;\n\treadonly isTty: boolean;\n\treadonly isCi: boolean;\n}\n\n/** §17.1/§17.4 gate: settings, outcome mapping, duration floor, and surface. */\nexport function shouldPlayCompletionSound(input: {\n\treadonly settings: ResolvedCompletionSoundSettings;\n\treadonly event: PromptSettledEvent;\n\treadonly surface: CompletionSoundSurface;\n}): boolean {\n\tconst { settings, event, surface } = input;\n\tif (!settings.enabled || !surface.isTui || !surface.isTty || surface.isCi) {\n\t\treturn false;\n\t}\n\tif (event.durationMs < settings.minDurationMs) {\n\t\treturn false;\n\t}\n\tif (event.outcome === \"completed\") {\n\t\treturn settings.onSuccess;\n\t}\n\tif (event.outcome === \"failed\") {\n\t\treturn settings.onFailure;\n\t}\n\t// User-initiated aborts never chime: the user is already at the keyboard.\n\treturn false;\n}\n\nexport interface CompletionSoundCandidate {\n\treadonly backend: CompletionSoundBackend;\n\t/** Fixed argv (§22.1); empty for terminal-bell (written, not spawned). */\n\treadonly argv: readonly string[];\n}\n\n/**\n * Deterministic backend candidate chain per platform (§17.3, §23.2 property\n * 14). The runner tries candidates in order on spawn failure.\n */\nexport function selectCompletionSoundCandidates(input: {\n\treadonly platform: NodeJS.Platform;\n\treadonly isWsl: boolean;\n\treadonly terminalBellFallback: boolean;\n}): readonly CompletionSoundCandidate[] {\n\tconst bell: CompletionSoundCandidate[] = input.terminalBellFallback ? [{ backend: \"terminal-bell\", argv: [] }] : [];\n\tif (input.platform === \"darwin\") {\n\t\treturn [{ backend: \"macos-afplay\", argv: [\"afplay\", \"/System/Library/Sounds/Glass.aiff\"] }, ...bell];\n\t}\n\tif (input.platform === \"win32\") {\n\t\treturn [{ backend: \"windows-system-sound\", argv: windowsSystemSoundArgv(\"powershell.exe\") }, ...bell];\n\t}\n\tif (input.platform === \"linux\" && input.isWsl) {\n\t\t// §17.3 WSL: Windows PowerShell first, then the terminal BEL.\n\t\treturn [{ backend: \"windows-system-sound\", argv: windowsSystemSoundArgv(\"powershell.exe\") }, ...bell];\n\t}\n\tif (input.platform === \"linux\") {\n\t\treturn [\n\t\t\t{ backend: \"linux-canberra\", argv: [\"canberra-gtk-play\", \"-i\", \"complete\"] },\n\t\t\t{ backend: \"linux-paplay\", argv: [\"paplay\", \"/usr/share/sounds/freedesktop/stereo/complete.oga\"] },\n\t\t\t{ backend: \"linux-aplay\", argv: [\"aplay\", \"-q\", \"/usr/share/sounds/alsa/Front_Center.wav\"] },\n\t\t\t...bell,\n\t\t];\n\t}\n\treturn [...bell];\n}\n\n/** Fixed expression only (§17.3 Windows): no user text ever enters the command. */\nfunction windowsSystemSoundArgv(executable: string): readonly string[] {\n\treturn [executable, \"-NoProfile\", \"-NonInteractive\", \"-Command\", \"[System.Media.SystemSounds]::Asterisk.Play()\"];\n}\n\nexport interface CompletionSoundIo {\n\t/** Spawn a fixed argv; resolve success/failure. Must never throw synchronously into the caller. */\n\treadonly spawnBackend: (argv: readonly string[]) => Promise<{ readonly ok: boolean; readonly diagnostic?: string }>;\n\treadonly writeBell: () => boolean;\n}\n\n/** Default IO: detached-ish fire-and-forget spawn per §17.4. */\nexport function defaultCompletionSoundIo(): CompletionSoundIo {\n\treturn {\n\t\tspawnBackend: (argv) =>\n\t\t\tnew Promise((resolve) => {\n\t\t\t\ttry {\n\t\t\t\t\tconst [executable, ...args] = argv;\n\t\t\t\t\tconst child = spawn(executable, args, { stdio: \"ignore\", shell: false, windowsHide: true });\n\t\t\t\t\tconst killTimer = setTimeout(() => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tchild.kill();\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t// Best effort only.\n\t\t\t\t\t\t}\n\t\t\t\t\t}, SPAWN_KILL_TIMEOUT_MS);\n\t\t\t\t\tkillTimer.unref();\n\t\t\t\t\tchild.once(\"error\", (error: NodeJS.ErrnoException) =>\n\t\t\t\t\t\tresolve({ ok: false, diagnostic: error.code ?? \"spawn_error\" }),\n\t\t\t\t\t);\n\t\t\t\t\t// Success means \"spawned\"; playback is not awaited (§17.4).\n\t\t\t\t\tchild.once(\"spawn\", () => {\n\t\t\t\t\t\tchild.unref();\n\t\t\t\t\t\tresolve({ ok: true });\n\t\t\t\t\t});\n\t\t\t\t} catch {\n\t\t\t\t\tresolve({ ok: false, diagnostic: \"spawn_error\" });\n\t\t\t\t}\n\t\t\t}),\n\t\twriteBell: () => {\n\t\t\ttry {\n\t\t\t\tprocess.stdout.write(\"\\u0007\");\n\t\t\t\treturn true;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t},\n\t};\n}\n\n/**\n * Stateful service: once per promptRunId (§17.4), deterministic candidate\n * walk, all failures downgraded to diagnostics. `handleSettled` is fire and\n * forget — callers must not await it on the prompt path.\n */\nexport interface CompletionSoundServiceDeps {\n\treadonly getSettings: () => CompletionSoundSettings | undefined;\n\treadonly surface: () => CompletionSoundSurface;\n\treadonly candidates: () => readonly CompletionSoundCandidate[];\n\treadonly io?: CompletionSoundIo;\n\treadonly env?: NodeJS.ProcessEnv;\n\t/** §20.2 observability hook; fail-open, never affects the result. */\n\treadonly onResult?: (result: CompletionSoundResult) => void;\n}\n\nexport class CompletionSoundService {\n\tprivate readonly playedRunIds = new Set<string>();\n\tprivate lastResult: CompletionSoundResult | null = null;\n\tprivate readonly deps: CompletionSoundServiceDeps;\n\n\tconstructor(deps: CompletionSoundServiceDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\t/** Most recent attempt, for diagnostics/tests. */\n\tget lastPlaybackResult(): CompletionSoundResult | null {\n\t\treturn this.lastResult;\n\t}\n\n\tasync handleSettled(event: PromptSettledEvent): Promise<CompletionSoundResult> {\n\t\ttry {\n\t\t\tconst settings = resolveCompletionSoundSettings(this.deps.getSettings(), this.deps.env ?? process.env);\n\t\t\tif (this.playedRunIds.has(event.promptRunId)) {\n\t\t\t\treturn this.finish({ backend: \"none\", attempted: false, success: false, diagnostic: \"already_played\" });\n\t\t\t}\n\t\t\tif (!shouldPlayCompletionSound({ settings, event, surface: this.deps.surface() })) {\n\t\t\t\treturn this.finish({ backend: \"none\", attempted: false, success: true });\n\t\t\t}\n\t\t\tthis.playedRunIds.add(event.promptRunId);\n\t\t\tconst io = this.deps.io ?? defaultCompletionSoundIo();\n\t\t\tconst diagnostics: string[] = [];\n\t\t\tfor (const candidate of this.deps.candidates()) {\n\t\t\t\tif (candidate.backend === \"terminal-bell\") {\n\t\t\t\t\tconst ok = io.writeBell();\n\t\t\t\t\treturn this.finish({\n\t\t\t\t\t\tbackend: \"terminal-bell\",\n\t\t\t\t\t\tattempted: true,\n\t\t\t\t\t\tsuccess: ok,\n\t\t\t\t\t\tdiagnostic: diagnostics.length > 0 ? diagnostics.join(\", \") : undefined,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tconst spawned = await io.spawnBackend(candidate.argv);\n\t\t\t\tif (spawned.ok) {\n\t\t\t\t\treturn this.finish({ backend: candidate.backend, attempted: true, success: true });\n\t\t\t\t}\n\t\t\t\tdiagnostics.push(`${candidate.backend}: ${spawned.diagnostic ?? \"failed\"}`);\n\t\t\t}\n\t\t\treturn this.finish({ backend: \"none\", attempted: true, success: false, diagnostic: diagnostics.join(\", \") });\n\t\t} catch (error) {\n\t\t\t// §23.2 property 13: sound failure never changes the prompt outcome.\n\t\t\treturn this.finish({ backend: \"none\", attempted: true, success: false, diagnostic: String(error) });\n\t\t}\n\t}\n\n\tprivate finish(result: CompletionSoundResult): CompletionSoundResult {\n\t\tthis.lastResult = result;\n\t\ttry {\n\t\t\tthis.deps.onResult?.(result);\n\t\t} catch {\n\t\t\t// Observability must never affect the sound path.\n\t\t}\n\t\treturn result;\n\t}\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-budget-system-prompt-items.d.ts","sourceRoot":"","sources":["../../src/core/context-budget-system-prompt-items.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,mBAAmB,EAAY,MAAM,iCAAiC,CAAC;AAErF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,UAAU,2BAA2B;IACpC,QAAQ,CAAC,YAAY,EAAE,SAAS,WAAW,EAAE,CAAC;IAC9C,QAAQ,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,OAAO,EAAE;QACjB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAC9C,QAAQ,CAAC,qBAAqB,CAAC,EAAE,OAAO,CAAC;QACzC,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;QAC3C,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;QACpC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;KAC/B,CAAC;CACF;AAQD,wBAAgB,6BAA6B,CAC5C,KAAK,EAAE,2BAA2B,EAClC,cAAc,EAAE,MAAM,GACpB,mBAAmB,EAAE,CAqBvB;AAyID,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE7C","sourcesContent":["import { pathToFileURL } from \"node:url\";\nimport { type ContextBudgetItemV2, fnv1aHex } from \"./context-budget-governor-v2.ts\";\nimport { scoreContextFileRelevance, scoreSkillRelevance } from \"./context-budget-relevance.ts\";\nimport type { ContextFile } from \"./resource-loader.ts\";\nimport type { Skill } from \"./skills.ts\";\n\ninterface SystemPromptBudgetItemInput {\n\treadonly contextFiles: readonly ContextFile[];\n\treadonly skills: readonly Skill[];\n\treadonly includeSkills: boolean;\n\treadonly options: {\n\t\treadonly activeSkillNames?: readonly string[];\n\t\treadonly includeSkillInventory?: boolean;\n\t\treadonly includeFullContextFiles?: boolean;\n\t\treadonly maxInactiveSkills?: number;\n\t\treadonly queryContext?: string;\n\t};\n}\n\nconst CONTEXT_EXCERPT_CHARS = 1200;\nconst MIN_EXCERPT_CHARS = 400;\nconst MAX_EXCERPT_CHARS = 3000;\nconst MAX_INACTIVE_SKILLS = 15;\nconst CONTEXT_FULL_BUDGET_RATIO = 0.5;\n\nexport function createSystemPromptBudgetItems(\n\tinput: SystemPromptBudgetItemInput,\n\tresourceBudget: number,\n): ContextBudgetItemV2[] {\n\tconst items: ContextBudgetItemV2[] = [];\n\tconst contextFullCount = input.options.includeFullContextFiles !== false ? input.contextFiles.length : 0;\n\tconst excerptChars = computeExcerptChars(resourceBudget, contextFullCount);\n\n\tfor (const contextFile of input.contextFiles) {\n\t\tconst queryScore = scoreContextFileRelevance(\n\t\t\t{ path: contextFile.path, content: contextFile.content, isGlobal: contextFile.isGlobal ?? false },\n\t\t\tinput.options.queryContext,\n\t\t);\n\t\tpushContextItems(items, contextFile, queryScore, excerptChars, input.options.includeFullContextFiles !== false);\n\t}\n\n\tconst activeSkillNames = new Set(input.options.activeSkillNames ?? []);\n\tconst visibleSkills = input.skills.filter(\n\t\t(skill) => !skill.disableModelInvocation || activeSkillNames.has(skill.name),\n\t);\n\tif (input.includeSkills && input.options.includeSkillInventory !== false && visibleSkills.length > 0) {\n\t\tpushSkillItems(items, visibleSkills, input.options);\n\t}\n\treturn items;\n}\n\nfunction pushContextItems(\n\titems: ContextBudgetItemV2[],\n\tcontextFile: ContextFile,\n\tqueryScore: number,\n\texcerptChars: number,\n\tincludeFullContextFiles: boolean,\n): void {\n\tconst scope = contextFile.isGlobal ? \"parent\" : \"project\";\n\tconst tagName = contextFile.isGlobal ? \"parent_instructions\" : \"project_instructions\";\n\titems.push({\n\t\tid: `context-pointer:${contextFile.path}`,\n\t\ttier: contextFile.isGlobal ? \"system\" : \"current-files\",\n\t\tpriority: contextFile.isGlobal ? \"hard\" : \"high\",\n\t\trelevance: (contextFile.isGlobal ? 1 : 0.8) * 0.4 + queryScore * 0.6,\n\t\ttext: renderContextPointer(contextFile, scope),\n\t});\n\tif (!includeFullContextFiles) return;\n\titems.push({\n\t\tid: `context-full:${contextFile.path}`,\n\t\ttier: contextFile.isGlobal ? \"system\" : \"current-files\",\n\t\tpriority: contextFile.isGlobal ? \"high\" : \"medium\",\n\t\trelevance: (contextFile.isGlobal ? 0.9 : 0.6) * 0.4 + queryScore * 0.6,\n\t\tredundancyKey: contextFile.path,\n\t\tsourceRef: {\n\t\t\turi: pathToFileURL(contextFile.path).href,\n\t\t\tcontentHash: fnv1aHex(contextFile.content),\n\t\t\tretrievable: true,\n\t\t},\n\t\ttext: renderContextFull(contextFile, tagName, excerptChars),\n\t});\n}\n\nfunction pushSkillItems(\n\titems: ContextBudgetItemV2[],\n\tvisibleSkills: readonly Skill[],\n\toptions: SystemPromptBudgetItemInput[\"options\"],\n): void {\n\titems.push({ id: \"skill-header\", tier: \"skills\", priority: \"hard\", text: renderSkillHeader() });\n\tconst activeSkillNames = new Set(options.activeSkillNames ?? []);\n\tconst maxInactive = options.maxInactiveSkills ?? MAX_INACTIVE_SKILLS;\n\tfor (const skill of visibleSkills.filter((skill) => activeSkillNames.has(skill.name))) {\n\t\titems.push(renderSkillBudgetItem(skill, \"hard\", 1));\n\t}\n\n\tconst sortedInactive = visibleSkills\n\t\t.filter((skill) => !activeSkillNames.has(skill.name))\n\t\t.sort((a, b) => scoreSkillRelevance(b, options.queryContext) - scoreSkillRelevance(a, options.queryContext));\n\tfor (const skill of sortedInactive.slice(0, maxInactive)) {\n\t\titems.push(renderSkillBudgetItem(skill, \"low\", Math.max(0.25, scoreSkillRelevance(skill, options.queryContext))));\n\t}\n\titems.push({ id: \"skill-footer\", tier: \"skills\", priority: \"hard\", text: \"</available_skills>\" });\n\n\tconst omittedInactiveCount = sortedInactive.length - Math.min(sortedInactive.length, maxInactive);\n\tif (omittedInactiveCount > 0) {\n\t\titems.push({\n\t\t\tid: \"skill-omitted-summary\",\n\t\t\ttier: \"skills\",\n\t\t\tpriority: \"low\",\n\t\t\trelevance: 0.1,\n\t\t\ttext: `<!-- ${omittedInactiveCount} additional skills available. Use 'read' to load a specific skill file when a task matches. -->`,\n\t\t});\n\t}\n}\n\nfunction renderSkillBudgetItem(skill: Skill, priority: \"hard\" | \"low\", relevance: number): ContextBudgetItemV2 {\n\treturn {\n\t\tid: `skill:${skill.name}`,\n\t\ttier: \"skills\",\n\t\tpriority,\n\t\trelevance,\n\t\tredundancyKey: skill.name,\n\t\ttext: renderSkillEntry(skill, priority === \"hard\"),\n\t};\n}\n\nfunction computeExcerptChars(resourceBudget: number, contextFullCount: number): number {\n\tif (contextFullCount <= 0) {\n\t\treturn CONTEXT_EXCERPT_CHARS;\n\t}\n\tconst allocatedTokens = Math.floor(resourceBudget * CONTEXT_FULL_BUDGET_RATIO) / contextFullCount;\n\tconst estimatedChars = Math.floor(allocatedTokens * 4);\n\treturn Math.max(MIN_EXCERPT_CHARS, Math.min(MAX_EXCERPT_CHARS, estimatedChars));\n}\n\nfunction renderContextPointer(contextFile: ContextFile, scope: string): string {\n\tconst marker = contextFile.containsJailbreak ? \" sanitized=true\" : \"\";\n\treturn `<context_file_pointer scope=\"${escapeXml(scope)}\" path=\"${escapeXml(contextFile.path)}\"${marker} chars=\"${contextFile.content.length}\" />`;\n}\n\nfunction renderContextFull(contextFile: ContextFile, tagName: string, excerptChars = CONTEXT_EXCERPT_CHARS): string {\n\tconst content =\n\t\tcontextFile.content.length > excerptChars\n\t\t\t? boundedExcerpt(contextFile.content, excerptChars)\n\t\t\t: contextFile.content;\n\tconst truncated = content.length < contextFile.content.length ? \" truncated=true\" : \"\";\n\treturn `<${tagName} path=\"${escapeXml(contextFile.path)}\"${truncated}>\\n${escapeXmlText(content)}\\n</${tagName}>`;\n}\n\nfunction boundedExcerpt(content: string, excerptChars = CONTEXT_EXCERPT_CHARS): string {\n\tconst headChars = Math.floor(excerptChars * 0.7);\n\tconst tailChars = excerptChars - headChars;\n\tlet head = content.slice(0, headChars);\n\tconst lastSpace = head.search(/\\s+\\S*$/);\n\tif (lastSpace > headChars * 0.5) {\n\t\thead = head.slice(0, lastSpace);\n\t}\n\tlet tail = content.slice(-tailChars);\n\tconst firstSpace = tail.search(/\\S+\\s+/);\n\tif (firstSpace > 0 && firstSpace < tailChars * 0.5) {\n\t\ttail = tail.slice(firstSpace).trimStart();\n\t}\n\treturn `${head}\\n\\n[...context-budget excerpt omitted ${content.length - excerptChars} chars...]\\n\\n${tail}`;\n}\n\nfunction renderSkillHeader(): string {\n\treturn [\n\t\t\"\\n\\nThe following skills provide specialized instructions for specific tasks.\",\n\t\t\"Use the read tool to load a skill's file when the task matches its description.\",\n\t\t\"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.\",\n\t\t\"\",\n\t\t\"<available_skills>\",\n\t].join(\"\\n\");\n}\n\nfunction renderSkillEntry(skill: Skill,
|
|
1
|
+
{"version":3,"file":"context-budget-system-prompt-items.d.ts","sourceRoot":"","sources":["../../src/core/context-budget-system-prompt-items.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,mBAAmB,EAAY,MAAM,iCAAiC,CAAC;AAErF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,UAAU,2BAA2B;IACpC,QAAQ,CAAC,YAAY,EAAE,SAAS,WAAW,EAAE,CAAC;IAC9C,QAAQ,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,OAAO,EAAE;QACjB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAC9C,QAAQ,CAAC,qBAAqB,CAAC,EAAE,OAAO,CAAC;QACzC,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;QAC3C,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;QACpC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;KAC/B,CAAC;CACF;AAQD,wBAAgB,6BAA6B,CAC5C,KAAK,EAAE,2BAA2B,EAClC,cAAc,EAAE,MAAM,GACpB,mBAAmB,EAAE,CAqBvB;AAyID,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE7C","sourcesContent":["import { pathToFileURL } from \"node:url\";\nimport { type ContextBudgetItemV2, fnv1aHex } from \"./context-budget-governor-v2.ts\";\nimport { scoreContextFileRelevance, scoreSkillRelevance } from \"./context-budget-relevance.ts\";\nimport type { ContextFile } from \"./resource-loader.ts\";\nimport type { Skill } from \"./skills.ts\";\n\ninterface SystemPromptBudgetItemInput {\n\treadonly contextFiles: readonly ContextFile[];\n\treadonly skills: readonly Skill[];\n\treadonly includeSkills: boolean;\n\treadonly options: {\n\t\treadonly activeSkillNames?: readonly string[];\n\t\treadonly includeSkillInventory?: boolean;\n\t\treadonly includeFullContextFiles?: boolean;\n\t\treadonly maxInactiveSkills?: number;\n\t\treadonly queryContext?: string;\n\t};\n}\n\nconst CONTEXT_EXCERPT_CHARS = 1200;\nconst MIN_EXCERPT_CHARS = 400;\nconst MAX_EXCERPT_CHARS = 3000;\nconst MAX_INACTIVE_SKILLS = 15;\nconst CONTEXT_FULL_BUDGET_RATIO = 0.5;\n\nexport function createSystemPromptBudgetItems(\n\tinput: SystemPromptBudgetItemInput,\n\tresourceBudget: number,\n): ContextBudgetItemV2[] {\n\tconst items: ContextBudgetItemV2[] = [];\n\tconst contextFullCount = input.options.includeFullContextFiles !== false ? input.contextFiles.length : 0;\n\tconst excerptChars = computeExcerptChars(resourceBudget, contextFullCount);\n\n\tfor (const contextFile of input.contextFiles) {\n\t\tconst queryScore = scoreContextFileRelevance(\n\t\t\t{ path: contextFile.path, content: contextFile.content, isGlobal: contextFile.isGlobal ?? false },\n\t\t\tinput.options.queryContext,\n\t\t);\n\t\tpushContextItems(items, contextFile, queryScore, excerptChars, input.options.includeFullContextFiles !== false);\n\t}\n\n\tconst activeSkillNames = new Set(input.options.activeSkillNames ?? []);\n\tconst visibleSkills = input.skills.filter(\n\t\t(skill) => !skill.disableModelInvocation || activeSkillNames.has(skill.name),\n\t);\n\tif (input.includeSkills && input.options.includeSkillInventory !== false && visibleSkills.length > 0) {\n\t\tpushSkillItems(items, visibleSkills, input.options);\n\t}\n\treturn items;\n}\n\nfunction pushContextItems(\n\titems: ContextBudgetItemV2[],\n\tcontextFile: ContextFile,\n\tqueryScore: number,\n\texcerptChars: number,\n\tincludeFullContextFiles: boolean,\n): void {\n\tconst scope = contextFile.isGlobal ? \"parent\" : \"project\";\n\tconst tagName = contextFile.isGlobal ? \"parent_instructions\" : \"project_instructions\";\n\titems.push({\n\t\tid: `context-pointer:${contextFile.path}`,\n\t\ttier: contextFile.isGlobal ? \"system\" : \"current-files\",\n\t\tpriority: contextFile.isGlobal ? \"hard\" : \"high\",\n\t\trelevance: (contextFile.isGlobal ? 1 : 0.8) * 0.4 + queryScore * 0.6,\n\t\ttext: renderContextPointer(contextFile, scope),\n\t});\n\tif (!includeFullContextFiles) return;\n\titems.push({\n\t\tid: `context-full:${contextFile.path}`,\n\t\ttier: contextFile.isGlobal ? \"system\" : \"current-files\",\n\t\tpriority: contextFile.isGlobal ? \"high\" : \"medium\",\n\t\trelevance: (contextFile.isGlobal ? 0.9 : 0.6) * 0.4 + queryScore * 0.6,\n\t\tredundancyKey: contextFile.path,\n\t\tsourceRef: {\n\t\t\turi: pathToFileURL(contextFile.path).href,\n\t\t\tcontentHash: fnv1aHex(contextFile.content),\n\t\t\tretrievable: true,\n\t\t},\n\t\ttext: renderContextFull(contextFile, tagName, excerptChars),\n\t});\n}\n\nfunction pushSkillItems(\n\titems: ContextBudgetItemV2[],\n\tvisibleSkills: readonly Skill[],\n\toptions: SystemPromptBudgetItemInput[\"options\"],\n): void {\n\titems.push({ id: \"skill-header\", tier: \"skills\", priority: \"hard\", text: renderSkillHeader() });\n\tconst activeSkillNames = new Set(options.activeSkillNames ?? []);\n\tconst maxInactive = options.maxInactiveSkills ?? MAX_INACTIVE_SKILLS;\n\tfor (const skill of visibleSkills.filter((skill) => activeSkillNames.has(skill.name))) {\n\t\titems.push(renderSkillBudgetItem(skill, \"hard\", 1));\n\t}\n\n\tconst sortedInactive = visibleSkills\n\t\t.filter((skill) => !activeSkillNames.has(skill.name))\n\t\t.sort((a, b) => scoreSkillRelevance(b, options.queryContext) - scoreSkillRelevance(a, options.queryContext));\n\tfor (const skill of sortedInactive.slice(0, maxInactive)) {\n\t\titems.push(renderSkillBudgetItem(skill, \"low\", Math.max(0.25, scoreSkillRelevance(skill, options.queryContext))));\n\t}\n\titems.push({ id: \"skill-footer\", tier: \"skills\", priority: \"hard\", text: \"</available_skills>\" });\n\n\tconst omittedInactiveCount = sortedInactive.length - Math.min(sortedInactive.length, maxInactive);\n\tif (omittedInactiveCount > 0) {\n\t\titems.push({\n\t\t\tid: \"skill-omitted-summary\",\n\t\t\ttier: \"skills\",\n\t\t\tpriority: \"low\",\n\t\t\trelevance: 0.1,\n\t\t\ttext: `<!-- ${omittedInactiveCount} additional skills available. Use 'read' to load a specific skill file when a task matches. -->`,\n\t\t});\n\t}\n}\n\nfunction renderSkillBudgetItem(skill: Skill, priority: \"hard\" | \"low\", relevance: number): ContextBudgetItemV2 {\n\treturn {\n\t\tid: `skill:${skill.name}`,\n\t\ttier: \"skills\",\n\t\tpriority,\n\t\trelevance,\n\t\tredundancyKey: skill.name,\n\t\ttext: renderSkillEntry(skill, priority === \"hard\"),\n\t};\n}\n\nfunction computeExcerptChars(resourceBudget: number, contextFullCount: number): number {\n\tif (contextFullCount <= 0) {\n\t\treturn CONTEXT_EXCERPT_CHARS;\n\t}\n\tconst allocatedTokens = Math.floor(resourceBudget * CONTEXT_FULL_BUDGET_RATIO) / contextFullCount;\n\tconst estimatedChars = Math.floor(allocatedTokens * 4);\n\treturn Math.max(MIN_EXCERPT_CHARS, Math.min(MAX_EXCERPT_CHARS, estimatedChars));\n}\n\nfunction renderContextPointer(contextFile: ContextFile, scope: string): string {\n\tconst marker = contextFile.containsJailbreak ? \" sanitized=true\" : \"\";\n\treturn `<context_file_pointer scope=\"${escapeXml(scope)}\" path=\"${escapeXml(contextFile.path)}\"${marker} chars=\"${contextFile.content.length}\" />`;\n}\n\nfunction renderContextFull(contextFile: ContextFile, tagName: string, excerptChars = CONTEXT_EXCERPT_CHARS): string {\n\tconst content =\n\t\tcontextFile.content.length > excerptChars\n\t\t\t? boundedExcerpt(contextFile.content, excerptChars)\n\t\t\t: contextFile.content;\n\tconst truncated = content.length < contextFile.content.length ? \" truncated=true\" : \"\";\n\treturn `<${tagName} path=\"${escapeXml(contextFile.path)}\"${truncated}>\\n${escapeXmlText(content)}\\n</${tagName}>`;\n}\n\nfunction boundedExcerpt(content: string, excerptChars = CONTEXT_EXCERPT_CHARS): string {\n\tconst headChars = Math.floor(excerptChars * 0.7);\n\tconst tailChars = excerptChars - headChars;\n\tlet head = content.slice(0, headChars);\n\tconst lastSpace = head.search(/\\s+\\S*$/);\n\tif (lastSpace > headChars * 0.5) {\n\t\thead = head.slice(0, lastSpace);\n\t}\n\tlet tail = content.slice(-tailChars);\n\tconst firstSpace = tail.search(/\\S+\\s+/);\n\tif (firstSpace > 0 && firstSpace < tailChars * 0.5) {\n\t\ttail = tail.slice(firstSpace).trimStart();\n\t}\n\treturn `${head}\\n\\n[...context-budget excerpt omitted ${content.length - excerptChars} chars...]\\n\\n${tail}`;\n}\n\nfunction renderSkillHeader(): string {\n\treturn [\n\t\t\"\\n\\nThe following skills provide specialized instructions for specific tasks.\",\n\t\t\"Use the read tool to load a skill's file when the task matches its description.\",\n\t\t\"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.\",\n\t\t\"\",\n\t\t\"<available_skills>\",\n\t].join(\"\\n\");\n}\n\nfunction renderSkillEntry(skill: Skill, active = false): string {\n\treturn [\n\t\t\" <skill>\",\n\t\t` <name>${escapeXml(skill.name)}</name>`,\n\t\t` <description>${escapeXml(skill.description)}</description>`,\n\t\t...(active ? [\" <activation>active</activation>\"] : []),\n\t\t` <location>${escapeXml(skill.filePath)}</location>`,\n\t\t\" </skill>\",\n\t].join(\"\\n\");\n}\n\nexport function escapeXmlText(str: string): string {\n\treturn str.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\");\n}\n\nexport function escapeXml(str: string): string {\n\treturn escapeXmlText(str).replace(/\"/g, \""\").replace(/'/g, \"'\");\n}\n"]}
|
|
@@ -125,12 +125,12 @@ function renderSkillHeader() {
|
|
|
125
125
|
"<available_skills>",
|
|
126
126
|
].join("\n");
|
|
127
127
|
}
|
|
128
|
-
function renderSkillEntry(skill,
|
|
128
|
+
function renderSkillEntry(skill, active = false) {
|
|
129
129
|
return [
|
|
130
130
|
" <skill>",
|
|
131
131
|
` <name>${escapeXml(skill.name)}</name>`,
|
|
132
132
|
` <description>${escapeXml(skill.description)}</description>`,
|
|
133
|
-
...(
|
|
133
|
+
...(active ? [" <activation>active</activation>"] : []),
|
|
134
134
|
` <location>${escapeXml(skill.filePath)}</location>`,
|
|
135
135
|
" </skill>",
|
|
136
136
|
].join("\n");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-budget-system-prompt-items.js","sourceRoot":"","sources":["../../src/core/context-budget-system-prompt-items.ts"],"names":[],"mappings":"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","sourcesContent":["import { pathToFileURL } from \"node:url\";\nimport { type ContextBudgetItemV2, fnv1aHex } from \"./context-budget-governor-v2.ts\";\nimport { scoreContextFileRelevance, scoreSkillRelevance } from \"./context-budget-relevance.ts\";\nimport type { ContextFile } from \"./resource-loader.ts\";\nimport type { Skill } from \"./skills.ts\";\n\ninterface SystemPromptBudgetItemInput {\n\treadonly contextFiles: readonly ContextFile[];\n\treadonly skills: readonly Skill[];\n\treadonly includeSkills: boolean;\n\treadonly options: {\n\t\treadonly activeSkillNames?: readonly string[];\n\t\treadonly includeSkillInventory?: boolean;\n\t\treadonly includeFullContextFiles?: boolean;\n\t\treadonly maxInactiveSkills?: number;\n\t\treadonly queryContext?: string;\n\t};\n}\n\nconst CONTEXT_EXCERPT_CHARS = 1200;\nconst MIN_EXCERPT_CHARS = 400;\nconst MAX_EXCERPT_CHARS = 3000;\nconst MAX_INACTIVE_SKILLS = 15;\nconst CONTEXT_FULL_BUDGET_RATIO = 0.5;\n\nexport function createSystemPromptBudgetItems(\n\tinput: SystemPromptBudgetItemInput,\n\tresourceBudget: number,\n): ContextBudgetItemV2[] {\n\tconst items: ContextBudgetItemV2[] = [];\n\tconst contextFullCount = input.options.includeFullContextFiles !== false ? input.contextFiles.length : 0;\n\tconst excerptChars = computeExcerptChars(resourceBudget, contextFullCount);\n\n\tfor (const contextFile of input.contextFiles) {\n\t\tconst queryScore = scoreContextFileRelevance(\n\t\t\t{ path: contextFile.path, content: contextFile.content, isGlobal: contextFile.isGlobal ?? false },\n\t\t\tinput.options.queryContext,\n\t\t);\n\t\tpushContextItems(items, contextFile, queryScore, excerptChars, input.options.includeFullContextFiles !== false);\n\t}\n\n\tconst activeSkillNames = new Set(input.options.activeSkillNames ?? []);\n\tconst visibleSkills = input.skills.filter(\n\t\t(skill) => !skill.disableModelInvocation || activeSkillNames.has(skill.name),\n\t);\n\tif (input.includeSkills && input.options.includeSkillInventory !== false && visibleSkills.length > 0) {\n\t\tpushSkillItems(items, visibleSkills, input.options);\n\t}\n\treturn items;\n}\n\nfunction pushContextItems(\n\titems: ContextBudgetItemV2[],\n\tcontextFile: ContextFile,\n\tqueryScore: number,\n\texcerptChars: number,\n\tincludeFullContextFiles: boolean,\n): void {\n\tconst scope = contextFile.isGlobal ? \"parent\" : \"project\";\n\tconst tagName = contextFile.isGlobal ? \"parent_instructions\" : \"project_instructions\";\n\titems.push({\n\t\tid: `context-pointer:${contextFile.path}`,\n\t\ttier: contextFile.isGlobal ? \"system\" : \"current-files\",\n\t\tpriority: contextFile.isGlobal ? \"hard\" : \"high\",\n\t\trelevance: (contextFile.isGlobal ? 1 : 0.8) * 0.4 + queryScore * 0.6,\n\t\ttext: renderContextPointer(contextFile, scope),\n\t});\n\tif (!includeFullContextFiles) return;\n\titems.push({\n\t\tid: `context-full:${contextFile.path}`,\n\t\ttier: contextFile.isGlobal ? \"system\" : \"current-files\",\n\t\tpriority: contextFile.isGlobal ? \"high\" : \"medium\",\n\t\trelevance: (contextFile.isGlobal ? 0.9 : 0.6) * 0.4 + queryScore * 0.6,\n\t\tredundancyKey: contextFile.path,\n\t\tsourceRef: {\n\t\t\turi: pathToFileURL(contextFile.path).href,\n\t\t\tcontentHash: fnv1aHex(contextFile.content),\n\t\t\tretrievable: true,\n\t\t},\n\t\ttext: renderContextFull(contextFile, tagName, excerptChars),\n\t});\n}\n\nfunction pushSkillItems(\n\titems: ContextBudgetItemV2[],\n\tvisibleSkills: readonly Skill[],\n\toptions: SystemPromptBudgetItemInput[\"options\"],\n): void {\n\titems.push({ id: \"skill-header\", tier: \"skills\", priority: \"hard\", text: renderSkillHeader() });\n\tconst activeSkillNames = new Set(options.activeSkillNames ?? []);\n\tconst maxInactive = options.maxInactiveSkills ?? MAX_INACTIVE_SKILLS;\n\tfor (const skill of visibleSkills.filter((skill) => activeSkillNames.has(skill.name))) {\n\t\titems.push(renderSkillBudgetItem(skill, \"hard\", 1));\n\t}\n\n\tconst sortedInactive = visibleSkills\n\t\t.filter((skill) => !activeSkillNames.has(skill.name))\n\t\t.sort((a, b) => scoreSkillRelevance(b, options.queryContext) - scoreSkillRelevance(a, options.queryContext));\n\tfor (const skill of sortedInactive.slice(0, maxInactive)) {\n\t\titems.push(renderSkillBudgetItem(skill, \"low\", Math.max(0.25, scoreSkillRelevance(skill, options.queryContext))));\n\t}\n\titems.push({ id: \"skill-footer\", tier: \"skills\", priority: \"hard\", text: \"</available_skills>\" });\n\n\tconst omittedInactiveCount = sortedInactive.length - Math.min(sortedInactive.length, maxInactive);\n\tif (omittedInactiveCount > 0) {\n\t\titems.push({\n\t\t\tid: \"skill-omitted-summary\",\n\t\t\ttier: \"skills\",\n\t\t\tpriority: \"low\",\n\t\t\trelevance: 0.1,\n\t\t\ttext: `<!-- ${omittedInactiveCount} additional skills available. Use 'read' to load a specific skill file when a task matches. -->`,\n\t\t});\n\t}\n}\n\nfunction renderSkillBudgetItem(skill: Skill, priority: \"hard\" | \"low\", relevance: number): ContextBudgetItemV2 {\n\treturn {\n\t\tid: `skill:${skill.name}`,\n\t\ttier: \"skills\",\n\t\tpriority,\n\t\trelevance,\n\t\tredundancyKey: skill.name,\n\t\ttext: renderSkillEntry(skill, priority === \"hard\"),\n\t};\n}\n\nfunction computeExcerptChars(resourceBudget: number, contextFullCount: number): number {\n\tif (contextFullCount <= 0) {\n\t\treturn CONTEXT_EXCERPT_CHARS;\n\t}\n\tconst allocatedTokens = Math.floor(resourceBudget * CONTEXT_FULL_BUDGET_RATIO) / contextFullCount;\n\tconst estimatedChars = Math.floor(allocatedTokens * 4);\n\treturn Math.max(MIN_EXCERPT_CHARS, Math.min(MAX_EXCERPT_CHARS, estimatedChars));\n}\n\nfunction renderContextPointer(contextFile: ContextFile, scope: string): string {\n\tconst marker = contextFile.containsJailbreak ? \" sanitized=true\" : \"\";\n\treturn `<context_file_pointer scope=\"${escapeXml(scope)}\" path=\"${escapeXml(contextFile.path)}\"${marker} chars=\"${contextFile.content.length}\" />`;\n}\n\nfunction renderContextFull(contextFile: ContextFile, tagName: string, excerptChars = CONTEXT_EXCERPT_CHARS): string {\n\tconst content =\n\t\tcontextFile.content.length > excerptChars\n\t\t\t? boundedExcerpt(contextFile.content, excerptChars)\n\t\t\t: contextFile.content;\n\tconst truncated = content.length < contextFile.content.length ? \" truncated=true\" : \"\";\n\treturn `<${tagName} path=\"${escapeXml(contextFile.path)}\"${truncated}>\\n${escapeXmlText(content)}\\n</${tagName}>`;\n}\n\nfunction boundedExcerpt(content: string, excerptChars = CONTEXT_EXCERPT_CHARS): string {\n\tconst headChars = Math.floor(excerptChars * 0.7);\n\tconst tailChars = excerptChars - headChars;\n\tlet head = content.slice(0, headChars);\n\tconst lastSpace = head.search(/\\s+\\S*$/);\n\tif (lastSpace > headChars * 0.5) {\n\t\thead = head.slice(0, lastSpace);\n\t}\n\tlet tail = content.slice(-tailChars);\n\tconst firstSpace = tail.search(/\\S+\\s+/);\n\tif (firstSpace > 0 && firstSpace < tailChars * 0.5) {\n\t\ttail = tail.slice(firstSpace).trimStart();\n\t}\n\treturn `${head}\\n\\n[...context-budget excerpt omitted ${content.length - excerptChars} chars...]\\n\\n${tail}`;\n}\n\nfunction renderSkillHeader(): string {\n\treturn [\n\t\t\"\\n\\nThe following skills provide specialized instructions for specific tasks.\",\n\t\t\"Use the read tool to load a skill's file when the task matches its description.\",\n\t\t\"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.\",\n\t\t\"\",\n\t\t\"<available_skills>\",\n\t].join(\"\\n\");\n}\n\nfunction renderSkillEntry(skill: Skill, operatorDefault = false): string {\n\treturn [\n\t\t\" <skill>\",\n\t\t` <name>${escapeXml(skill.name)}</name>`,\n\t\t` <description>${escapeXml(skill.description)}</description>`,\n\t\t...(operatorDefault ? [\" <activation>operator-default</activation>\"] : []),\n\t\t` <location>${escapeXml(skill.filePath)}</location>`,\n\t\t\" </skill>\",\n\t].join(\"\\n\");\n}\n\nexport function escapeXmlText(str: string): string {\n\treturn str.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\");\n}\n\nexport function escapeXml(str: string): string {\n\treturn escapeXmlText(str).replace(/\"/g, \""\").replace(/'/g, \"'\");\n}\n"]}
|
|
1
|
+
{"version":3,"file":"context-budget-system-prompt-items.js","sourceRoot":"","sources":["../../src/core/context-budget-system-prompt-items.ts"],"names":[],"mappings":"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","sourcesContent":["import { pathToFileURL } from \"node:url\";\nimport { type ContextBudgetItemV2, fnv1aHex } from \"./context-budget-governor-v2.ts\";\nimport { scoreContextFileRelevance, scoreSkillRelevance } from \"./context-budget-relevance.ts\";\nimport type { ContextFile } from \"./resource-loader.ts\";\nimport type { Skill } from \"./skills.ts\";\n\ninterface SystemPromptBudgetItemInput {\n\treadonly contextFiles: readonly ContextFile[];\n\treadonly skills: readonly Skill[];\n\treadonly includeSkills: boolean;\n\treadonly options: {\n\t\treadonly activeSkillNames?: readonly string[];\n\t\treadonly includeSkillInventory?: boolean;\n\t\treadonly includeFullContextFiles?: boolean;\n\t\treadonly maxInactiveSkills?: number;\n\t\treadonly queryContext?: string;\n\t};\n}\n\nconst CONTEXT_EXCERPT_CHARS = 1200;\nconst MIN_EXCERPT_CHARS = 400;\nconst MAX_EXCERPT_CHARS = 3000;\nconst MAX_INACTIVE_SKILLS = 15;\nconst CONTEXT_FULL_BUDGET_RATIO = 0.5;\n\nexport function createSystemPromptBudgetItems(\n\tinput: SystemPromptBudgetItemInput,\n\tresourceBudget: number,\n): ContextBudgetItemV2[] {\n\tconst items: ContextBudgetItemV2[] = [];\n\tconst contextFullCount = input.options.includeFullContextFiles !== false ? input.contextFiles.length : 0;\n\tconst excerptChars = computeExcerptChars(resourceBudget, contextFullCount);\n\n\tfor (const contextFile of input.contextFiles) {\n\t\tconst queryScore = scoreContextFileRelevance(\n\t\t\t{ path: contextFile.path, content: contextFile.content, isGlobal: contextFile.isGlobal ?? false },\n\t\t\tinput.options.queryContext,\n\t\t);\n\t\tpushContextItems(items, contextFile, queryScore, excerptChars, input.options.includeFullContextFiles !== false);\n\t}\n\n\tconst activeSkillNames = new Set(input.options.activeSkillNames ?? []);\n\tconst visibleSkills = input.skills.filter(\n\t\t(skill) => !skill.disableModelInvocation || activeSkillNames.has(skill.name),\n\t);\n\tif (input.includeSkills && input.options.includeSkillInventory !== false && visibleSkills.length > 0) {\n\t\tpushSkillItems(items, visibleSkills, input.options);\n\t}\n\treturn items;\n}\n\nfunction pushContextItems(\n\titems: ContextBudgetItemV2[],\n\tcontextFile: ContextFile,\n\tqueryScore: number,\n\texcerptChars: number,\n\tincludeFullContextFiles: boolean,\n): void {\n\tconst scope = contextFile.isGlobal ? \"parent\" : \"project\";\n\tconst tagName = contextFile.isGlobal ? \"parent_instructions\" : \"project_instructions\";\n\titems.push({\n\t\tid: `context-pointer:${contextFile.path}`,\n\t\ttier: contextFile.isGlobal ? \"system\" : \"current-files\",\n\t\tpriority: contextFile.isGlobal ? \"hard\" : \"high\",\n\t\trelevance: (contextFile.isGlobal ? 1 : 0.8) * 0.4 + queryScore * 0.6,\n\t\ttext: renderContextPointer(contextFile, scope),\n\t});\n\tif (!includeFullContextFiles) return;\n\titems.push({\n\t\tid: `context-full:${contextFile.path}`,\n\t\ttier: contextFile.isGlobal ? \"system\" : \"current-files\",\n\t\tpriority: contextFile.isGlobal ? \"high\" : \"medium\",\n\t\trelevance: (contextFile.isGlobal ? 0.9 : 0.6) * 0.4 + queryScore * 0.6,\n\t\tredundancyKey: contextFile.path,\n\t\tsourceRef: {\n\t\t\turi: pathToFileURL(contextFile.path).href,\n\t\t\tcontentHash: fnv1aHex(contextFile.content),\n\t\t\tretrievable: true,\n\t\t},\n\t\ttext: renderContextFull(contextFile, tagName, excerptChars),\n\t});\n}\n\nfunction pushSkillItems(\n\titems: ContextBudgetItemV2[],\n\tvisibleSkills: readonly Skill[],\n\toptions: SystemPromptBudgetItemInput[\"options\"],\n): void {\n\titems.push({ id: \"skill-header\", tier: \"skills\", priority: \"hard\", text: renderSkillHeader() });\n\tconst activeSkillNames = new Set(options.activeSkillNames ?? []);\n\tconst maxInactive = options.maxInactiveSkills ?? MAX_INACTIVE_SKILLS;\n\tfor (const skill of visibleSkills.filter((skill) => activeSkillNames.has(skill.name))) {\n\t\titems.push(renderSkillBudgetItem(skill, \"hard\", 1));\n\t}\n\n\tconst sortedInactive = visibleSkills\n\t\t.filter((skill) => !activeSkillNames.has(skill.name))\n\t\t.sort((a, b) => scoreSkillRelevance(b, options.queryContext) - scoreSkillRelevance(a, options.queryContext));\n\tfor (const skill of sortedInactive.slice(0, maxInactive)) {\n\t\titems.push(renderSkillBudgetItem(skill, \"low\", Math.max(0.25, scoreSkillRelevance(skill, options.queryContext))));\n\t}\n\titems.push({ id: \"skill-footer\", tier: \"skills\", priority: \"hard\", text: \"</available_skills>\" });\n\n\tconst omittedInactiveCount = sortedInactive.length - Math.min(sortedInactive.length, maxInactive);\n\tif (omittedInactiveCount > 0) {\n\t\titems.push({\n\t\t\tid: \"skill-omitted-summary\",\n\t\t\ttier: \"skills\",\n\t\t\tpriority: \"low\",\n\t\t\trelevance: 0.1,\n\t\t\ttext: `<!-- ${omittedInactiveCount} additional skills available. Use 'read' to load a specific skill file when a task matches. -->`,\n\t\t});\n\t}\n}\n\nfunction renderSkillBudgetItem(skill: Skill, priority: \"hard\" | \"low\", relevance: number): ContextBudgetItemV2 {\n\treturn {\n\t\tid: `skill:${skill.name}`,\n\t\ttier: \"skills\",\n\t\tpriority,\n\t\trelevance,\n\t\tredundancyKey: skill.name,\n\t\ttext: renderSkillEntry(skill, priority === \"hard\"),\n\t};\n}\n\nfunction computeExcerptChars(resourceBudget: number, contextFullCount: number): number {\n\tif (contextFullCount <= 0) {\n\t\treturn CONTEXT_EXCERPT_CHARS;\n\t}\n\tconst allocatedTokens = Math.floor(resourceBudget * CONTEXT_FULL_BUDGET_RATIO) / contextFullCount;\n\tconst estimatedChars = Math.floor(allocatedTokens * 4);\n\treturn Math.max(MIN_EXCERPT_CHARS, Math.min(MAX_EXCERPT_CHARS, estimatedChars));\n}\n\nfunction renderContextPointer(contextFile: ContextFile, scope: string): string {\n\tconst marker = contextFile.containsJailbreak ? \" sanitized=true\" : \"\";\n\treturn `<context_file_pointer scope=\"${escapeXml(scope)}\" path=\"${escapeXml(contextFile.path)}\"${marker} chars=\"${contextFile.content.length}\" />`;\n}\n\nfunction renderContextFull(contextFile: ContextFile, tagName: string, excerptChars = CONTEXT_EXCERPT_CHARS): string {\n\tconst content =\n\t\tcontextFile.content.length > excerptChars\n\t\t\t? boundedExcerpt(contextFile.content, excerptChars)\n\t\t\t: contextFile.content;\n\tconst truncated = content.length < contextFile.content.length ? \" truncated=true\" : \"\";\n\treturn `<${tagName} path=\"${escapeXml(contextFile.path)}\"${truncated}>\\n${escapeXmlText(content)}\\n</${tagName}>`;\n}\n\nfunction boundedExcerpt(content: string, excerptChars = CONTEXT_EXCERPT_CHARS): string {\n\tconst headChars = Math.floor(excerptChars * 0.7);\n\tconst tailChars = excerptChars - headChars;\n\tlet head = content.slice(0, headChars);\n\tconst lastSpace = head.search(/\\s+\\S*$/);\n\tif (lastSpace > headChars * 0.5) {\n\t\thead = head.slice(0, lastSpace);\n\t}\n\tlet tail = content.slice(-tailChars);\n\tconst firstSpace = tail.search(/\\S+\\s+/);\n\tif (firstSpace > 0 && firstSpace < tailChars * 0.5) {\n\t\ttail = tail.slice(firstSpace).trimStart();\n\t}\n\treturn `${head}\\n\\n[...context-budget excerpt omitted ${content.length - excerptChars} chars...]\\n\\n${tail}`;\n}\n\nfunction renderSkillHeader(): string {\n\treturn [\n\t\t\"\\n\\nThe following skills provide specialized instructions for specific tasks.\",\n\t\t\"Use the read tool to load a skill's file when the task matches its description.\",\n\t\t\"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.\",\n\t\t\"\",\n\t\t\"<available_skills>\",\n\t].join(\"\\n\");\n}\n\nfunction renderSkillEntry(skill: Skill, active = false): string {\n\treturn [\n\t\t\" <skill>\",\n\t\t` <name>${escapeXml(skill.name)}</name>`,\n\t\t` <description>${escapeXml(skill.description)}</description>`,\n\t\t...(active ? [\" <activation>active</activation>\"] : []),\n\t\t` <location>${escapeXml(skill.filePath)}</location>`,\n\t\t\" </skill>\",\n\t].join(\"\\n\");\n}\n\nexport function escapeXmlText(str: string): string {\n\treturn str.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\");\n}\n\nexport function escapeXml(str: string): string {\n\treturn escapeXmlText(str).replace(/\"/g, \""\").replace(/'/g, \"'\");\n}\n"]}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { DEFAULT_CPU_SAMPLE_MS, MAX_CPU_SAMPLE_MS, MIN_CPU_SAMPLE_MS } from "./system-cpu-sampler.ts";
|
|
2
|
+
export { DEFAULT_CPU_SAMPLE_MS, MAX_CPU_SAMPLE_MS, MIN_CPU_SAMPLE_MS };
|
|
3
|
+
/** Diagnostic hint only — admission authority comes from measured values (§4.1). */
|
|
4
|
+
export type HostEnvironment = "native" | "wsl" | "container" | "unknown";
|
|
5
|
+
/** Injectable probe sources for deterministic, fault-injectable tests. */
|
|
6
|
+
export interface HostResourceProbeIo {
|
|
7
|
+
readonly platform: () => NodeJS.Platform;
|
|
8
|
+
readonly arch: () => string;
|
|
9
|
+
readonly cpuCount: () => number;
|
|
10
|
+
readonly availableMemory: () => number | null | undefined;
|
|
11
|
+
readonly constrainedMemory: () => number | null | undefined;
|
|
12
|
+
readonly totalmem: () => number;
|
|
13
|
+
readonly freemem: () => number;
|
|
14
|
+
readonly rssBytes: () => number;
|
|
15
|
+
readonly heapStatistics: () => {
|
|
16
|
+
readonly heapUsedBytes: number;
|
|
17
|
+
readonly heapLimitBytes: number;
|
|
18
|
+
};
|
|
19
|
+
readonly statfs: (target: string) => Promise<{
|
|
20
|
+
readonly bavail: bigint;
|
|
21
|
+
readonly bsize: bigint;
|
|
22
|
+
}>;
|
|
23
|
+
readonly sampleSystemCpuPercent: (sampleMs: number) => Promise<number | null>;
|
|
24
|
+
readonly osRelease: () => string;
|
|
25
|
+
readonly fileExists: (target: string) => boolean;
|
|
26
|
+
}
|
|
27
|
+
export type HostResourceProbeOptions = {
|
|
28
|
+
/** Workspace directory for the disk probe. Used for measurement only; never recorded. */
|
|
29
|
+
cwd?: string;
|
|
30
|
+
/** Hard preflight deadline for async probes; default 300 ms, clamped `[50, 5000]` (§6.3, §18.1). */
|
|
31
|
+
maxProbeMs?: number;
|
|
32
|
+
/** CPU two-sample interval; clamped `[150, 250]` ms (§6.3). */
|
|
33
|
+
cpuSampleMs?: number;
|
|
34
|
+
/** Injectable clock for a deterministic `observedAt`. */
|
|
35
|
+
now?: () => Date;
|
|
36
|
+
/** Partial probe source overrides; unspecified sources use real process/OS reads. */
|
|
37
|
+
io?: Partial<HostResourceProbeIo>;
|
|
38
|
+
};
|
|
39
|
+
export declare const DEFAULT_MAX_PROBE_MS = 300;
|
|
40
|
+
export declare const MIN_MAX_PROBE_MS = 50;
|
|
41
|
+
export declare const MAX_MAX_PROBE_MS = 5000;
|
|
42
|
+
export declare function defaultProbeIo(): HostResourceProbeIo;
|
|
43
|
+
/** Coarse environment hint (§4.1). Never an admission authority; `unknown` on any probe error. */
|
|
44
|
+
export declare function detectEnvironment(io: HostResourceProbeIo): HostEnvironment;
|
|
45
|
+
type DeadlineResult<T> = {
|
|
46
|
+
readonly value: T | null;
|
|
47
|
+
readonly timedOut: boolean;
|
|
48
|
+
readonly failed: boolean;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Race `run()` against a hard deadline. Never rejects. The bounded deadline
|
|
52
|
+
* timer is intentionally ref'd (see module doc) and cleared once settled, so
|
|
53
|
+
* no timer outlives the probe.
|
|
54
|
+
*/
|
|
55
|
+
export declare function raceDeadline<T>(run: () => Promise<T>, deadlineMs: number): Promise<DeadlineResult<T>>;
|
|
56
|
+
/** `bavail * bsize` with BigInt overflow clamped to `Number.MAX_SAFE_INTEGER` (§6.5). */
|
|
57
|
+
export declare function bigintProductToSafeNumber(a: bigint, b: bigint): number | null;
|
|
58
|
+
export declare function guardValue<T>(read: () => T, fallback: T): T;
|
|
59
|
+
export declare function probePositiveNumber(read: () => number | null | undefined, diagnostics: string[], code: string): number | null;
|
|
60
|
+
export declare function probePositiveInteger(read: () => number, diagnostics: string[], code: string): number | null;
|
|
61
|
+
export declare function sanitizeNonNegative(value: number | undefined): number | null;
|
|
62
|
+
export declare function clampInteger(value: unknown, fallback: number, min: number, max: number): number;
|
|
63
|
+
//# sourceMappingURL=host-resource-probe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-resource-probe.d.ts","sourceRoot":"","sources":["../../src/core/host-resource-probe.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as v8 from \"node:v8\";\nimport {\n\tDEFAULT_CPU_SAMPLE_MS,\n\tMAX_CPU_SAMPLE_MS,\n\tMIN_CPU_SAMPLE_MS,\n\tsampleSystemCpuPercent,\n} from \"./system-cpu-sampler.ts\";\n\nexport { DEFAULT_CPU_SAMPLE_MS, MAX_CPU_SAMPLE_MS, MIN_CPU_SAMPLE_MS };\n\n/** Diagnostic hint only — admission authority comes from measured values (§4.1). */\nexport type HostEnvironment = \"native\" | \"wsl\" | \"container\" | \"unknown\";\n\n/** Injectable probe sources for deterministic, fault-injectable tests. */\nexport interface HostResourceProbeIo {\n\treadonly platform: () => NodeJS.Platform;\n\treadonly arch: () => string;\n\treadonly cpuCount: () => number;\n\treadonly availableMemory: () => number | null | undefined;\n\treadonly constrainedMemory: () => number | null | undefined;\n\treadonly totalmem: () => number;\n\treadonly freemem: () => number;\n\treadonly rssBytes: () => number;\n\treadonly heapStatistics: () => { readonly heapUsedBytes: number; readonly heapLimitBytes: number };\n\treadonly statfs: (target: string) => Promise<{ readonly bavail: bigint; readonly bsize: bigint }>;\n\treadonly sampleSystemCpuPercent: (sampleMs: number) => Promise<number | null>;\n\treadonly osRelease: () => string;\n\treadonly fileExists: (target: string) => boolean;\n}\n\nexport type HostResourceProbeOptions = {\n\t/** Workspace directory for the disk probe. Used for measurement only; never recorded. */\n\tcwd?: string;\n\t/** Hard preflight deadline for async probes; default 300 ms, clamped `[50, 5000]` (§6.3, §18.1). */\n\tmaxProbeMs?: number;\n\t/** CPU two-sample interval; clamped `[150, 250]` ms (§6.3). */\n\tcpuSampleMs?: number;\n\t/** Injectable clock for a deterministic `observedAt`. */\n\tnow?: () => Date;\n\t/** Partial probe source overrides; unspecified sources use real process/OS reads. */\n\tio?: Partial<HostResourceProbeIo>;\n};\n\nexport const DEFAULT_MAX_PROBE_MS = 300;\nexport const MIN_MAX_PROBE_MS = 50;\nexport const MAX_MAX_PROBE_MS = 5000;\n\nexport function defaultProbeIo(): HostResourceProbeIo {\n\treturn {\n\t\tplatform: () => process.platform,\n\t\tarch: () => process.arch,\n\t\tcpuCount: () => os.cpus().length,\n\t\tavailableMemory: () => (typeof process.availableMemory === \"function\" ? process.availableMemory() : null),\n\t\tconstrainedMemory: () => (typeof process.constrainedMemory === \"function\" ? process.constrainedMemory() : null),\n\t\ttotalmem: () => os.totalmem(),\n\t\tfreemem: () => os.freemem(),\n\t\trssBytes: () => process.memoryUsage().rss,\n\t\theapStatistics: () => {\n\t\t\tconst stats = v8.getHeapStatistics();\n\t\t\treturn { heapUsedBytes: stats.used_heap_size, heapLimitBytes: stats.heap_size_limit };\n\t\t},\n\t\tstatfs: async (target) => {\n\t\t\tconst stats = await fs.promises.statfs(target, { bigint: true });\n\t\t\treturn { bavail: stats.bavail, bsize: stats.bsize };\n\t\t},\n\t\tsampleSystemCpuPercent: (sampleMs) => sampleSystemCpuPercent({ sampleMs }),\n\t\tosRelease: () => os.release(),\n\t\tfileExists: (target) => fs.existsSync(target),\n\t};\n}\n\n/** Coarse environment hint (§4.1). Never an admission authority; `unknown` on any probe error. */\nexport function detectEnvironment(io: HostResourceProbeIo): HostEnvironment {\n\ttry {\n\t\tif (io.platform() === \"linux\") {\n\t\t\tif (/microsoft/i.test(io.osRelease())) {\n\t\t\t\treturn \"wsl\";\n\t\t\t}\n\t\t\tif (io.fileExists(\"/.dockerenv\") || io.fileExists(\"/run/.containerenv\")) {\n\t\t\t\treturn \"container\";\n\t\t\t}\n\t\t}\n\t\treturn \"native\";\n\t} catch {\n\t\treturn \"unknown\";\n\t}\n}\n\ntype DeadlineResult<T> = { readonly value: T | null; readonly timedOut: boolean; readonly failed: boolean };\n\n/**\n * Race `run()` against a hard deadline. Never rejects. The bounded deadline\n * timer is intentionally ref'd (see module doc) and cleared once settled, so\n * no timer outlives the probe.\n */\nexport async function raceDeadline<T>(run: () => Promise<T>, deadlineMs: number): Promise<DeadlineResult<T>> {\n\tlet timer: ReturnType<typeof setTimeout> | null = null;\n\ttry {\n\t\tconst timeout = new Promise<\"timeout\">((resolve) => {\n\t\t\ttimer = setTimeout(() => resolve(\"timeout\"), Math.max(1, deadlineMs));\n\t\t});\n\t\tconst work = Promise.resolve()\n\t\t\t.then(run)\n\t\t\t.then(\n\t\t\t\t(value): DeadlineResult<T> => ({ value, timedOut: false, failed: false }),\n\t\t\t\t(): DeadlineResult<T> => ({ value: null, timedOut: false, failed: true }),\n\t\t\t);\n\t\tconst settled = await Promise.race([work, timeout]);\n\t\tif (settled === \"timeout\") {\n\t\t\treturn { value: null, timedOut: true, failed: false };\n\t\t}\n\t\treturn settled;\n\t} finally {\n\t\tif (timer !== null) {\n\t\t\tclearTimeout(timer);\n\t\t}\n\t}\n}\n\n/** `bavail * bsize` with BigInt overflow clamped to `Number.MAX_SAFE_INTEGER` (§6.5). */\nexport function bigintProductToSafeNumber(a: bigint, b: bigint): number | null {\n\tif (a < 0n || b < 0n) {\n\t\treturn null;\n\t}\n\tconst product = a * b;\n\tconst max = BigInt(Number.MAX_SAFE_INTEGER);\n\treturn Number(product > max ? max : product);\n}\n\nexport function guardValue<T>(read: () => T, fallback: T): T {\n\ttry {\n\t\treturn read();\n\t} catch {\n\t\treturn fallback;\n\t}\n}\n\nexport function probePositiveNumber(\n\tread: () => number | null | undefined,\n\tdiagnostics: string[],\n\tcode: string,\n): number | null {\n\ttry {\n\t\tconst value = read();\n\t\t// Values above MAX_SAFE_INTEGER are \"no limit\" sentinels, not real byte\n\t\t// counts — observed on WSL where process.constrainedMemory() reports the\n\t\t// cgroup u64 max (2^64). §6.4: unclear OS semantics degrade to a\n\t\t// diagnostic instead of entering the candidate set.\n\t\tif (typeof value !== \"number\" || !Number.isFinite(value) || value <= 0 || value > Number.MAX_SAFE_INTEGER) {\n\t\t\tdiagnostics.push(code);\n\t\t\treturn null;\n\t\t}\n\t\treturn value;\n\t} catch {\n\t\tdiagnostics.push(code);\n\t\treturn null;\n\t}\n}\n\nexport function probePositiveInteger(read: () => number, diagnostics: string[], code: string): number | null {\n\tconst value = probePositiveNumber(read, diagnostics, code);\n\treturn value === null ? null : Math.floor(value);\n}\n\nexport function sanitizeNonNegative(value: number | undefined): number | null {\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n\t\treturn null;\n\t}\n\treturn value;\n}\n\nexport function clampInteger(value: unknown, fallback: number, min: number, max: number): number {\n\tif (typeof value !== \"number\" || !Number.isFinite(value)) {\n\t\treturn fallback;\n\t}\n\treturn Math.floor(Math.max(min, Math.min(max, value)));\n}\n"]}
|