dsh-math-modeling-agent 0.2.8 → 0.4.1
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/README.md +16 -1
- package/package.json +1 -1
- package/skills/math-modeling-agent/SKILL.md +27 -14
- package/skills/math-modeling-agent/references/claims-evidence.md +3 -1
- package/skills/math-modeling-agent/references/interaction-protocol.md +13 -1
- package/skills/math-modeling-agent/references/math-grill.md +8 -0
- package/skills/math-modeling-agent/references/original-project-parity.md +19 -0
- package/skills/math-modeling-agent/references/report-contract.md +6 -0
- package/skills/math-modeling-agent/references/subagent-dispatch.md +3 -2
- package/skills/math-modeling-agent/references/tool-policy.md +11 -0
- package/skills/math-modeling-agent/references/workflow.md +32 -9
- package/skills/math-modeling-agent/scripts/computation/README.md +20 -0
- package/skills/math-modeling-agent/scripts/computation/backend-inventory.schema.json +78 -0
- package/skills/math-modeling-agent/scripts/computation/backend_inventory.ps1 +351 -0
- package/skills/math-modeling-agent/scripts/computation/backend_inventory.py +322 -0
- package/skills/math-modeling-agent/scripts/computation/computation_record.py +361 -0
- package/skills/math-modeling-agent/scripts/computation/probe_backends.ps1 +396 -0
- package/skills/math-modeling-agent/scripts/computation/probe_backends.py +230 -0
- package/skills/math-modeling-agent/scripts/distribution-parity.mjs +123 -0
- package/skills/math-modeling-agent/scripts/run-state.mjs +34 -7
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
[CmdletBinding()]
|
|
2
|
+
param(
|
|
3
|
+
[string]$PythonCommand = 'python',
|
|
4
|
+
[string]$SageCommand = '',
|
|
5
|
+
[string]$WslDistro = '',
|
|
6
|
+
[string]$WslSageCommand = 'sage',
|
|
7
|
+
[string]$PrimecountCommand = '',
|
|
8
|
+
[ValidateRange(100, 300000)][int]$TimeoutMilliseconds = 20000
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
$ErrorActionPreference = 'Stop'
|
|
12
|
+
|
|
13
|
+
function Resolve-CommandPath {
|
|
14
|
+
param([Parameter(Mandatory = $true)][string]$Name)
|
|
15
|
+
|
|
16
|
+
if (Test-Path -LiteralPath $Name -PathType Leaf) {
|
|
17
|
+
return (Get-Item -LiteralPath $Name).FullName
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
$command = Get-Command -Name $Name -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
21
|
+
if ($null -eq $command) {
|
|
22
|
+
return $null
|
|
23
|
+
}
|
|
24
|
+
return $command.Source
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function Limit-ProbeOutput {
|
|
28
|
+
param([AllowNull()][object]$Text)
|
|
29
|
+
|
|
30
|
+
$value = if ($null -eq $Text) { '' } else { [string]$Text }
|
|
31
|
+
$limit = 65536
|
|
32
|
+
if ($value.Length -gt $limit) {
|
|
33
|
+
return $value.Substring(0, $limit) + [Environment]::NewLine + '[output truncated]'
|
|
34
|
+
}
|
|
35
|
+
return $value
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function Get-CompletedText {
|
|
39
|
+
param([Parameter(Mandatory = $true)][object]$Task)
|
|
40
|
+
|
|
41
|
+
if ($Task.Wait(1000)) {
|
|
42
|
+
return [string]$Task.Result
|
|
43
|
+
}
|
|
44
|
+
return ''
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function Invoke-ProcessProbe {
|
|
48
|
+
param(
|
|
49
|
+
[Parameter(Mandatory = $true)][string]$Executable,
|
|
50
|
+
[Parameter(Mandatory = $true)][string[]]$Arguments
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
$startInfo = [Diagnostics.ProcessStartInfo]::new()
|
|
54
|
+
$startInfo.FileName = $Executable
|
|
55
|
+
$startInfo.UseShellExecute = $false
|
|
56
|
+
$startInfo.RedirectStandardOutput = $true
|
|
57
|
+
$startInfo.RedirectStandardError = $true
|
|
58
|
+
foreach ($argument in $Arguments) {
|
|
59
|
+
[void]$startInfo.ArgumentList.Add($argument)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
$process = [Diagnostics.Process]::new()
|
|
63
|
+
$timedOut = $false
|
|
64
|
+
try {
|
|
65
|
+
$process.StartInfo = $startInfo
|
|
66
|
+
if (-not $process.Start()) {
|
|
67
|
+
throw "Could not start probe: $Executable"
|
|
68
|
+
}
|
|
69
|
+
$stdoutTask = $process.StandardOutput.ReadToEndAsync()
|
|
70
|
+
$stderrTask = $process.StandardError.ReadToEndAsync()
|
|
71
|
+
if (-not $process.WaitForExit($TimeoutMilliseconds)) {
|
|
72
|
+
$timedOut = $true
|
|
73
|
+
try { $process.Kill($true) } catch { }
|
|
74
|
+
[void]$process.WaitForExit(1000)
|
|
75
|
+
}
|
|
76
|
+
$stdout = Limit-ProbeOutput (Get-CompletedText $stdoutTask)
|
|
77
|
+
$stderr = Limit-ProbeOutput (Get-CompletedText $stderrTask)
|
|
78
|
+
$combined = ((@($stdout, $stderr) | Where-Object { $_ }) -join [Environment]::NewLine).Trim()
|
|
79
|
+
if ($timedOut) {
|
|
80
|
+
return [ordered]@{
|
|
81
|
+
status = 'probe_failed'
|
|
82
|
+
stdout = $stdout
|
|
83
|
+
stderr = $stderr
|
|
84
|
+
version_output = $combined
|
|
85
|
+
exit_code = $null
|
|
86
|
+
error = "probe timed out after $TimeoutMilliseconds ms"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
$exitCode = $process.ExitCode
|
|
90
|
+
return [ordered]@{
|
|
91
|
+
status = if ($exitCode -eq 0) { 'available' } else { 'probe_failed' }
|
|
92
|
+
stdout = $stdout
|
|
93
|
+
stderr = $stderr
|
|
94
|
+
version_output = $combined
|
|
95
|
+
exit_code = $exitCode
|
|
96
|
+
error = if ($exitCode -eq 0) { '' } else { $combined }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return [ordered]@{
|
|
101
|
+
status = 'probe_failed'
|
|
102
|
+
stdout = ''
|
|
103
|
+
stderr = ''
|
|
104
|
+
version_output = ''
|
|
105
|
+
exit_code = $null
|
|
106
|
+
error = $_.Exception.Message
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
$process.Dispose()
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function Invoke-VersionProbe {
|
|
115
|
+
param(
|
|
116
|
+
[Parameter(Mandatory = $true)][string]$Executable,
|
|
117
|
+
[Parameter(Mandatory = $true)][string[]]$Arguments
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return Invoke-ProcessProbe -Executable $Executable -Arguments $Arguments
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function Get-HostIdentity {
|
|
124
|
+
$system = if ([Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([Runtime.InteropServices.OSPlatform]::Windows)) {
|
|
125
|
+
'Windows'
|
|
126
|
+
}
|
|
127
|
+
elseif ([Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([Runtime.InteropServices.OSPlatform]::OSX)) {
|
|
128
|
+
'Darwin'
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
'Linux'
|
|
132
|
+
}
|
|
133
|
+
$rawArchitecture = [Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant()
|
|
134
|
+
$architecture = switch ($rawArchitecture) {
|
|
135
|
+
'x64' { 'x86_64' }
|
|
136
|
+
'amd64' { 'x86_64' }
|
|
137
|
+
'arm64' { 'arm64' }
|
|
138
|
+
'x86' { 'x86' }
|
|
139
|
+
default { $rawArchitecture }
|
|
140
|
+
}
|
|
141
|
+
return [ordered]@{ system = $system; architecture = $architecture; powershell_edition = $PSVersionTable.PSEdition }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
$wolframInstallations = @()
|
|
145
|
+
$programRoots = @()
|
|
146
|
+
if ($env:ProgramFiles) {
|
|
147
|
+
$programRoots += (Join-Path $env:ProgramFiles 'Wolfram Research\Wolfram')
|
|
148
|
+
}
|
|
149
|
+
if (${env:ProgramFiles(x86)}) {
|
|
150
|
+
$programRoots += (Join-Path ${env:ProgramFiles(x86)} 'Wolfram Research\Wolfram')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
foreach ($programRoot in ($programRoots | Select-Object -Unique)) {
|
|
154
|
+
if (-not (Test-Path -LiteralPath $programRoot -PathType Container)) {
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
foreach ($versionDirectory in (Get-ChildItem -LiteralPath $programRoot -Directory -ErrorAction SilentlyContinue)) {
|
|
158
|
+
$wolframExecutable = Join-Path $versionDirectory.FullName 'wolfram.exe'
|
|
159
|
+
if (Test-Path -LiteralPath $wolframExecutable -PathType Leaf) {
|
|
160
|
+
$item = Get-Item -LiteralPath $wolframExecutable
|
|
161
|
+
$wolframInstallations += [ordered]@{
|
|
162
|
+
version_directory = $versionDirectory.Name
|
|
163
|
+
executable = $item.FullName
|
|
164
|
+
file_version = $item.VersionInfo.FileVersion
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
$wolframScriptPath = Resolve-CommandPath -Name 'wolframscript'
|
|
171
|
+
if (-not $wolframScriptPath -and $env:ProgramFiles) {
|
|
172
|
+
$knownWolframScript = Join-Path $env:ProgramFiles 'Wolfram Research\WolframScript\wolframscript.exe'
|
|
173
|
+
if (Test-Path -LiteralPath $knownWolframScript -PathType Leaf) {
|
|
174
|
+
$wolframScriptPath = (Get-Item -LiteralPath $knownWolframScript).FullName
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
$wolframScript = if ($wolframScriptPath) {
|
|
179
|
+
$probe = Invoke-VersionProbe -Executable $wolframScriptPath -Arguments @('-version')
|
|
180
|
+
[ordered]@{
|
|
181
|
+
status = $probe.status
|
|
182
|
+
path = $wolframScriptPath
|
|
183
|
+
version_output = $probe.version_output
|
|
184
|
+
exit_code = $probe.exit_code
|
|
185
|
+
error = $probe.error
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
[ordered]@{
|
|
190
|
+
status = 'unavailable'
|
|
191
|
+
path = $null
|
|
192
|
+
version_output = ''
|
|
193
|
+
exit_code = $null
|
|
194
|
+
error = ''
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
$primecountRequestedCommand = ''
|
|
199
|
+
$primecountDiscoverySource = ''
|
|
200
|
+
$primecountPath = $null
|
|
201
|
+
if ($PrimecountCommand) {
|
|
202
|
+
$primecountRequestedCommand = $PrimecountCommand
|
|
203
|
+
$primecountDiscoverySource = 'explicit'
|
|
204
|
+
$primecountPath = Resolve-CommandPath -Name $PrimecountCommand
|
|
205
|
+
}
|
|
206
|
+
elseif ($env:PRIMECOUNT_EXE) {
|
|
207
|
+
$primecountRequestedCommand = $env:PRIMECOUNT_EXE
|
|
208
|
+
$primecountDiscoverySource = 'environment'
|
|
209
|
+
$primecountPath = Resolve-CommandPath -Name $env:PRIMECOUNT_EXE
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
$primecountRequestedCommand = 'primecount'
|
|
213
|
+
$primecountDiscoverySource = 'path'
|
|
214
|
+
$primecountPath = Resolve-CommandPath -Name 'primecount'
|
|
215
|
+
if (-not $primecountPath -and $env:LOCALAPPDATA) {
|
|
216
|
+
$knownPrimecount = Join-Path $env:LOCALAPPDATA 'Programs\primecount\primecount.exe'
|
|
217
|
+
if (Test-Path -LiteralPath $knownPrimecount -PathType Leaf) {
|
|
218
|
+
$primecountPath = (Get-Item -LiteralPath $knownPrimecount).FullName
|
|
219
|
+
$primecountDiscoverySource = 'known_user_location'
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
$primecount = if ($primecountPath) {
|
|
225
|
+
$probe = Invoke-VersionProbe -Executable $primecountPath -Arguments @('--version')
|
|
226
|
+
[ordered]@{
|
|
227
|
+
status = $probe.status
|
|
228
|
+
requested_command = $primecountRequestedCommand
|
|
229
|
+
discovery_source = $primecountDiscoverySource
|
|
230
|
+
path = $primecountPath
|
|
231
|
+
version_output = $probe.version_output
|
|
232
|
+
exit_code = $probe.exit_code
|
|
233
|
+
error = $probe.error
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
[ordered]@{
|
|
238
|
+
status = 'unavailable'
|
|
239
|
+
requested_command = $primecountRequestedCommand
|
|
240
|
+
discovery_source = $primecountDiscoverySource
|
|
241
|
+
path = $null
|
|
242
|
+
version_output = ''
|
|
243
|
+
exit_code = $null
|
|
244
|
+
error = ''
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
$nativeSageName = if ($SageCommand) { $SageCommand } else { 'sage' }
|
|
249
|
+
$nativeSagePath = Resolve-CommandPath -Name $nativeSageName
|
|
250
|
+
$nativeSage = if ($nativeSagePath) {
|
|
251
|
+
$probe = Invoke-VersionProbe -Executable $nativeSagePath -Arguments @('--version')
|
|
252
|
+
[ordered]@{
|
|
253
|
+
status = $probe.status
|
|
254
|
+
requested_command = $nativeSageName
|
|
255
|
+
path = $nativeSagePath
|
|
256
|
+
version_output = $probe.version_output
|
|
257
|
+
exit_code = $probe.exit_code
|
|
258
|
+
error = $probe.error
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
[ordered]@{
|
|
263
|
+
status = 'unavailable'
|
|
264
|
+
requested_command = $nativeSageName
|
|
265
|
+
path = $null
|
|
266
|
+
version_output = ''
|
|
267
|
+
exit_code = $null
|
|
268
|
+
error = ''
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
$wslSage = [ordered]@{
|
|
273
|
+
status = 'not_requested'
|
|
274
|
+
distro = $WslDistro
|
|
275
|
+
requested_command = $WslSageCommand
|
|
276
|
+
version_output = ''
|
|
277
|
+
exit_code = $null
|
|
278
|
+
error = ''
|
|
279
|
+
}
|
|
280
|
+
if ($WslDistro) {
|
|
281
|
+
$wslPath = Resolve-CommandPath -Name 'wsl.exe'
|
|
282
|
+
if (-not $wslPath) {
|
|
283
|
+
$wslSage.status = 'wsl_unavailable'
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
$probe = Invoke-ProcessProbe -Executable $wslPath -Arguments @('-d', $WslDistro, '--', $WslSageCommand, '--version')
|
|
287
|
+
$wslSage.status = $probe.status
|
|
288
|
+
$wslSage.version_output = $probe.version_output
|
|
289
|
+
$wslSage.exit_code = $probe.exit_code
|
|
290
|
+
$wslSage.error = $probe.error
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
$pythonPath = Resolve-CommandPath -Name $PythonCommand
|
|
295
|
+
$pythonProbe = if ($pythonPath) {
|
|
296
|
+
$probeCode = @'
|
|
297
|
+
import importlib.metadata
|
|
298
|
+
import importlib.util
|
|
299
|
+
import json
|
|
300
|
+
import sys
|
|
301
|
+
|
|
302
|
+
modules = {
|
|
303
|
+
"numpy": "numpy",
|
|
304
|
+
"sympy": "sympy",
|
|
305
|
+
"scipy": "scipy",
|
|
306
|
+
"mpmath": "mpmath",
|
|
307
|
+
"sage": "sagemath-standard",
|
|
308
|
+
"sageall": "sagemath-standard",
|
|
309
|
+
}
|
|
310
|
+
libraries = {}
|
|
311
|
+
for module_name, distribution_name in modules.items():
|
|
312
|
+
available = importlib.util.find_spec(module_name) is not None
|
|
313
|
+
version = None
|
|
314
|
+
if available:
|
|
315
|
+
try:
|
|
316
|
+
version = importlib.metadata.version(distribution_name)
|
|
317
|
+
except importlib.metadata.PackageNotFoundError:
|
|
318
|
+
version = None
|
|
319
|
+
libraries[module_name] = {"available": available, "version": version}
|
|
320
|
+
print(json.dumps({
|
|
321
|
+
"python_version": sys.version.split()[0],
|
|
322
|
+
"executable": sys.executable,
|
|
323
|
+
"libraries": libraries,
|
|
324
|
+
}, ensure_ascii=False))
|
|
325
|
+
'@
|
|
326
|
+
$probe = Invoke-ProcessProbe -Executable $pythonPath -Arguments @('-c', $probeCode)
|
|
327
|
+
if ($probe.status -ne 'available') {
|
|
328
|
+
[ordered]@{
|
|
329
|
+
status = 'probe_failed'
|
|
330
|
+
requested_command = $PythonCommand
|
|
331
|
+
path = $pythonPath
|
|
332
|
+
version = ''
|
|
333
|
+
libraries = [ordered]@{}
|
|
334
|
+
exit_code = $probe.exit_code
|
|
335
|
+
error = $probe.error
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
try {
|
|
340
|
+
$data = $probe.stdout | ConvertFrom-Json -AsHashtable
|
|
341
|
+
[ordered]@{
|
|
342
|
+
status = 'available'
|
|
343
|
+
requested_command = $PythonCommand
|
|
344
|
+
path = $data.executable
|
|
345
|
+
version = $data.python_version
|
|
346
|
+
libraries = $data.libraries
|
|
347
|
+
exit_code = 0
|
|
348
|
+
error = ''
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
[ordered]@{
|
|
353
|
+
status = 'probe_failed'
|
|
354
|
+
requested_command = $PythonCommand
|
|
355
|
+
path = $pythonPath
|
|
356
|
+
version = ''
|
|
357
|
+
libraries = [ordered]@{}
|
|
358
|
+
exit_code = $probe.exit_code
|
|
359
|
+
error = $_.Exception.Message
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
[ordered]@{
|
|
366
|
+
status = 'unavailable'
|
|
367
|
+
requested_command = $PythonCommand
|
|
368
|
+
path = $null
|
|
369
|
+
version = ''
|
|
370
|
+
libraries = [ordered]@{}
|
|
371
|
+
exit_code = $null
|
|
372
|
+
error = ''
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
$result = [ordered]@{
|
|
377
|
+
schema_version = '1.0'
|
|
378
|
+
probed_at_utc = [DateTime]::UtcNow.ToString('o')
|
|
379
|
+
host = Get-HostIdentity
|
|
380
|
+
mathematica = [ordered]@{
|
|
381
|
+
installations = $wolframInstallations
|
|
382
|
+
wolframscript = $wolframScript
|
|
383
|
+
mcp = [ordered]@{
|
|
384
|
+
status = 'requires_agent_probe'
|
|
385
|
+
evidence = 'Call the configured Mathematica MCP from the agent and record the returned Wolfram Language version.'
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
primecount = $primecount
|
|
389
|
+
sagemath = [ordered]@{
|
|
390
|
+
native = $nativeSage
|
|
391
|
+
wsl = $wslSage
|
|
392
|
+
}
|
|
393
|
+
python = $pythonProbe
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
$result | ConvertTo-Json -Depth 10 -Compress
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Cross-platform local backend probe for math-science-computation."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import importlib.metadata
|
|
8
|
+
import importlib.util
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import platform
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def utc_now() -> str:
|
|
20
|
+
return datetime.now(timezone.utc).isoformat()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def normalized_architecture() -> str:
|
|
24
|
+
value = platform.machine().lower()
|
|
25
|
+
return {
|
|
26
|
+
"amd64": "x86_64",
|
|
27
|
+
"x64": "x86_64",
|
|
28
|
+
"aarch64": "arm64",
|
|
29
|
+
"arm64": "arm64",
|
|
30
|
+
"i386": "x86",
|
|
31
|
+
"i686": "x86",
|
|
32
|
+
"x86": "x86",
|
|
33
|
+
}.get(value, value or "unknown")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def resolve_command(name: str) -> str | None:
|
|
37
|
+
candidate = Path(name).expanduser()
|
|
38
|
+
if candidate.is_file():
|
|
39
|
+
return str(candidate.resolve())
|
|
40
|
+
return shutil.which(name)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def version_probe(executable: str, arguments: list[str]) -> dict:
|
|
44
|
+
try:
|
|
45
|
+
process = subprocess.run(
|
|
46
|
+
[executable, *arguments],
|
|
47
|
+
text=True,
|
|
48
|
+
capture_output=True,
|
|
49
|
+
check=False,
|
|
50
|
+
timeout=20,
|
|
51
|
+
)
|
|
52
|
+
output = "\n".join(part.strip() for part in (process.stdout, process.stderr) if part.strip())
|
|
53
|
+
return {
|
|
54
|
+
"status": "available" if process.returncode == 0 else "probe_failed",
|
|
55
|
+
"version_output": output,
|
|
56
|
+
"exit_code": process.returncode,
|
|
57
|
+
"error": "",
|
|
58
|
+
}
|
|
59
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
60
|
+
return {
|
|
61
|
+
"status": "probe_failed",
|
|
62
|
+
"version_output": "",
|
|
63
|
+
"exit_code": None,
|
|
64
|
+
"error": f"{type(exc).__name__}: {exc}",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def known_wolfram_installations(system: str) -> list[dict]:
|
|
69
|
+
candidates: list[Path] = []
|
|
70
|
+
if system == "Windows":
|
|
71
|
+
for variable in ("ProgramFiles", "ProgramFiles(x86)"):
|
|
72
|
+
root = os.environ.get(variable)
|
|
73
|
+
if root:
|
|
74
|
+
candidates.extend(Path(root, "Wolfram Research", "Wolfram").glob("*/Wolfram.exe"))
|
|
75
|
+
elif system == "Darwin":
|
|
76
|
+
applications = Path("/Applications")
|
|
77
|
+
for pattern in ("Mathematica*.app/Contents/MacOS/WolframKernel", "Wolfram*.app/Contents/MacOS/WolframKernel"):
|
|
78
|
+
candidates.extend(applications.glob(pattern))
|
|
79
|
+
elif system == "Linux":
|
|
80
|
+
for root in (Path("/usr/local/Wolfram"), Path("/opt/Wolfram")):
|
|
81
|
+
candidates.extend(root.glob("Mathematica/*/Executables/WolframKernel"))
|
|
82
|
+
candidates.extend(root.glob("Wolfram/*/Executables/WolframKernel"))
|
|
83
|
+
|
|
84
|
+
for command in ("WolframKernel", "wolfram", "math"):
|
|
85
|
+
path = resolve_command(command)
|
|
86
|
+
if path:
|
|
87
|
+
candidates.append(Path(path))
|
|
88
|
+
|
|
89
|
+
installations: list[dict] = []
|
|
90
|
+
seen: set[str] = set()
|
|
91
|
+
for candidate in candidates:
|
|
92
|
+
if not candidate.is_file():
|
|
93
|
+
continue
|
|
94
|
+
resolved = str(candidate.resolve())
|
|
95
|
+
key = os.path.normcase(resolved)
|
|
96
|
+
if key in seen:
|
|
97
|
+
continue
|
|
98
|
+
seen.add(key)
|
|
99
|
+
installations.append(
|
|
100
|
+
{
|
|
101
|
+
"version_directory": candidate.parent.name,
|
|
102
|
+
"executable": resolved,
|
|
103
|
+
"file_version": None,
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
return installations
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def probe_wolframscript(system: str) -> dict:
|
|
110
|
+
path = resolve_command("wolframscript")
|
|
111
|
+
if not path and system == "Windows":
|
|
112
|
+
root = os.environ.get("ProgramFiles")
|
|
113
|
+
candidate = Path(root, "Wolfram Research", "WolframScript", "wolframscript.exe") if root else None
|
|
114
|
+
if candidate and candidate.is_file():
|
|
115
|
+
path = str(candidate.resolve())
|
|
116
|
+
if not path:
|
|
117
|
+
return {"status": "unavailable", "path": None, "version_output": "", "exit_code": None, "error": ""}
|
|
118
|
+
result = version_probe(path, ["-version"])
|
|
119
|
+
return {"path": path, **result}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def probe_primecount(explicit: str) -> dict:
|
|
123
|
+
if explicit:
|
|
124
|
+
requested, source = explicit, "explicit"
|
|
125
|
+
elif os.environ.get("PRIMECOUNT_EXE"):
|
|
126
|
+
requested, source = os.environ["PRIMECOUNT_EXE"], "environment"
|
|
127
|
+
else:
|
|
128
|
+
requested, source = "primecount", "path"
|
|
129
|
+
path = resolve_command(requested)
|
|
130
|
+
if not path and source == "path" and platform.system() == "Windows":
|
|
131
|
+
local_app_data = os.environ.get("LOCALAPPDATA")
|
|
132
|
+
candidate = Path(local_app_data, "Programs", "primecount", "primecount.exe") if local_app_data else None
|
|
133
|
+
if candidate and candidate.is_file():
|
|
134
|
+
path, source = str(candidate.resolve()), "known_user_location"
|
|
135
|
+
base = {"requested_command": requested, "discovery_source": source, "path": path}
|
|
136
|
+
if not path:
|
|
137
|
+
return {"status": "unavailable", **base, "version_output": "", "exit_code": None, "error": ""}
|
|
138
|
+
return {**base, **version_probe(path, ["--version"])}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def probe_sage(command: str, wsl_distro: str, wsl_sage_command: str, system: str) -> dict:
|
|
142
|
+
requested = command or "sage"
|
|
143
|
+
path = resolve_command(requested)
|
|
144
|
+
native = {"requested_command": requested, "path": path}
|
|
145
|
+
if path:
|
|
146
|
+
native.update(version_probe(path, ["--version"]))
|
|
147
|
+
else:
|
|
148
|
+
native.update({"status": "unavailable", "version_output": "", "exit_code": None, "error": ""})
|
|
149
|
+
|
|
150
|
+
wsl = {
|
|
151
|
+
"status": "not_requested",
|
|
152
|
+
"distro": wsl_distro,
|
|
153
|
+
"requested_command": wsl_sage_command,
|
|
154
|
+
"version_output": "",
|
|
155
|
+
"exit_code": None,
|
|
156
|
+
"error": "",
|
|
157
|
+
}
|
|
158
|
+
if wsl_distro:
|
|
159
|
+
if system != "Windows":
|
|
160
|
+
wsl["status"] = "unsupported_platform"
|
|
161
|
+
else:
|
|
162
|
+
wsl_path = resolve_command("wsl.exe")
|
|
163
|
+
if not wsl_path:
|
|
164
|
+
wsl["status"] = "wsl_unavailable"
|
|
165
|
+
else:
|
|
166
|
+
result = version_probe(wsl_path, ["-d", wsl_distro, "--", wsl_sage_command, "--version"])
|
|
167
|
+
wsl.update(result)
|
|
168
|
+
return {"native": native, "wsl": wsl}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def probe_python(command: str) -> dict:
|
|
172
|
+
path = resolve_command(command)
|
|
173
|
+
base = {"requested_command": command, "path": path}
|
|
174
|
+
if not path:
|
|
175
|
+
return {"status": "unavailable", **base, "version": "", "libraries": {}, "exit_code": None, "error": ""}
|
|
176
|
+
probe_code = r'''
|
|
177
|
+
import importlib.metadata, importlib.util, json, sys
|
|
178
|
+
modules = {"numpy":"numpy","sympy":"sympy","scipy":"scipy","mpmath":"mpmath","sage":"sagemath-standard","sageall":"sagemath-standard"}
|
|
179
|
+
libraries = {}
|
|
180
|
+
for module_name, distribution_name in modules.items():
|
|
181
|
+
available = importlib.util.find_spec(module_name) is not None
|
|
182
|
+
version = None
|
|
183
|
+
if available:
|
|
184
|
+
try: version = importlib.metadata.version(distribution_name)
|
|
185
|
+
except importlib.metadata.PackageNotFoundError: pass
|
|
186
|
+
libraries[module_name] = {"available": available, "version": version}
|
|
187
|
+
print(json.dumps({"python_version":sys.version.split()[0],"executable":sys.executable,"libraries":libraries}, ensure_ascii=False))
|
|
188
|
+
'''
|
|
189
|
+
try:
|
|
190
|
+
process = subprocess.run([path, "-c", probe_code], text=True, capture_output=True, check=False, timeout=20)
|
|
191
|
+
if process.returncode != 0:
|
|
192
|
+
return {"status": "probe_failed", **base, "version": "", "libraries": {}, "exit_code": process.returncode, "error": (process.stderr or process.stdout).strip()}
|
|
193
|
+
data = json.loads(process.stdout)
|
|
194
|
+
return {"status": "available", "requested_command": command, "path": data["executable"], "version": data["python_version"], "libraries": data["libraries"], "exit_code": 0, "error": ""}
|
|
195
|
+
except (OSError, subprocess.SubprocessError, json.JSONDecodeError) as exc:
|
|
196
|
+
return {"status": "probe_failed", **base, "version": "", "libraries": {}, "exit_code": None, "error": f"{type(exc).__name__}: {exc}"}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
200
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
201
|
+
parser.add_argument("--python-command", default=sys.executable)
|
|
202
|
+
parser.add_argument("--sage-command", default="")
|
|
203
|
+
parser.add_argument("--wsl-distro", default="")
|
|
204
|
+
parser.add_argument("--wsl-sage-command", default="sage")
|
|
205
|
+
parser.add_argument("--primecount-command", default="")
|
|
206
|
+
return parser
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def main() -> int:
|
|
210
|
+
args = build_parser().parse_args()
|
|
211
|
+
system = platform.system()
|
|
212
|
+
result = {
|
|
213
|
+
"schema_version": "1.0",
|
|
214
|
+
"probed_at_utc": utc_now(),
|
|
215
|
+
"host": {"system": system, "architecture": normalized_architecture(), "python_implementation": platform.python_implementation()},
|
|
216
|
+
"mathematica": {
|
|
217
|
+
"installations": known_wolfram_installations(system),
|
|
218
|
+
"wolframscript": probe_wolframscript(system),
|
|
219
|
+
"mcp": {"status": "requires_agent_probe", "evidence": "Call the configured Mathematica MCP from the agent and record the returned Wolfram Language version."},
|
|
220
|
+
},
|
|
221
|
+
"primecount": probe_primecount(args.primecount_command),
|
|
222
|
+
"sagemath": probe_sage(args.sage_command, args.wsl_distro, args.wsl_sage_command, system),
|
|
223
|
+
"python": probe_python(args.python_command),
|
|
224
|
+
}
|
|
225
|
+
print(json.dumps(result, ensure_ascii=False, separators=(",", ":")))
|
|
226
|
+
return 0
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
if __name__ == "__main__":
|
|
230
|
+
raise SystemExit(main())
|