@wix/pathgrade 1.0.9 → 1.0.10

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.
@@ -1,5 +1,5 @@
1
- import type { MockMcpServerDescriptor } from '../core/mcp-mock.types.js';
2
- import type { AgentName, AgentOptions } from '../sdk/types.js';
1
+ import type { MockMcpServerDescriptor } from '../../core/mcp-mock.types.js';
2
+ import type { AgentName, AgentOptions } from '../../sdk/types.js';
3
3
  export declare const OPENCODE_MODEL = "anthropic/claude-sonnet-5";
4
4
  export declare const OPENCODE_VERSION = "1.18.18";
5
5
  export interface OpenCodeRuntimeLockEntry {
@@ -0,0 +1,3 @@
1
+ export declare function killOpenCodeProcessGroup(pid: number, signal: NodeJS.Signals): void;
2
+ export declare function registerOpenCodeProcessGroup(pid: number): void;
3
+ export declare function unregisterOpenCodeProcessGroup(pid: number): void;
@@ -0,0 +1,47 @@
1
+ /** Process-wide ownership of detached OpenCode child process groups. */
2
+ const activeProcessGroups = new Set();
3
+ let processCleanupInstalled = false;
4
+ export function killOpenCodeProcessGroup(pid, signal) {
5
+ try {
6
+ process.kill(-pid, signal);
7
+ }
8
+ catch {
9
+ // The process group may already have exited.
10
+ }
11
+ }
12
+ function killAllProcessGroups() {
13
+ for (const pid of activeProcessGroups) {
14
+ killOpenCodeProcessGroup(pid, 'SIGKILL');
15
+ }
16
+ activeProcessGroups.clear();
17
+ }
18
+ function uninstallProcessCleanup() {
19
+ if (!processCleanupInstalled)
20
+ return;
21
+ processCleanupInstalled = false;
22
+ process.removeListener('exit', onProcessExit);
23
+ process.removeListener('SIGINT', onProcessSignal);
24
+ process.removeListener('SIGTERM', onProcessSignal);
25
+ }
26
+ function onProcessExit() {
27
+ killAllProcessGroups();
28
+ }
29
+ function onProcessSignal(signal) {
30
+ killAllProcessGroups();
31
+ uninstallProcessCleanup();
32
+ process.kill(process.pid, signal);
33
+ }
34
+ export function registerOpenCodeProcessGroup(pid) {
35
+ activeProcessGroups.add(pid);
36
+ if (processCleanupInstalled)
37
+ return;
38
+ processCleanupInstalled = true;
39
+ process.once('exit', onProcessExit);
40
+ process.once('SIGINT', onProcessSignal);
41
+ process.once('SIGTERM', onProcessSignal);
42
+ }
43
+ export function unregisterOpenCodeProcessGroup(pid) {
44
+ activeProcessGroups.delete(pid);
45
+ if (activeProcessGroups.size === 0)
46
+ uninstallProcessCleanup();
47
+ }
@@ -7,7 +7,8 @@ import fs from 'fs-extra';
7
7
  import { BaseAgent, getRuntimeEnv, getWorkspacePath, } from '../types.js';
8
8
  import { buildSummary, enrichSkillEvents } from '../tool-events.js';
9
9
  import { readStagedMcpServers } from '../providers/mcp-config.js';
10
- import { currentOpenCodePlatformKey, OPENCODE_MODEL, OPENCODE_RUNTIME_LOCK, } from './opencode-contract.js';
10
+ import { currentOpenCodePlatformKey, OPENCODE_MODEL, OPENCODE_RUNTIME_LOCK, } from './opencode/contract.js';
11
+ import { killOpenCodeProcessGroup, registerOpenCodeProcessGroup, unregisterOpenCodeProcessGroup, } from './opencode/process-groups.js';
11
12
  const OUTPUT_CAP_BYTES = 16 * 1024 * 1024;
12
13
  const FIXED_OPENCODE_ENV = {
13
14
  OPENCODE_CLIENT: 'pathgrade',
@@ -68,6 +69,7 @@ export function spawnOpenCode(executable, args, options) {
68
69
  let aborted = false;
69
70
  let killTimer;
70
71
  let settled = false;
72
+ let terminating = false;
71
73
  const child = spawn(executable, args, {
72
74
  cwd: options.cwd,
73
75
  env: options.env,
@@ -75,22 +77,22 @@ export function spawnOpenCode(executable, args, options) {
75
77
  detached: true,
76
78
  stdio: ['pipe', 'pipe', 'pipe'],
77
79
  });
80
+ if (child.pid)
81
+ registerOpenCodeProcessGroup(child.pid);
78
82
  const killGroup = (signal) => {
79
83
  if (!child.pid)
80
84
  return;
81
- try {
82
- process.kill(-child.pid, signal);
83
- }
84
- catch {
85
- // The process may have exited between the state check and kill.
86
- }
85
+ killOpenCodeProcessGroup(child.pid, signal);
87
86
  };
88
87
  const terminate = () => {
88
+ terminating = true;
89
89
  killGroup('SIGTERM');
90
90
  killTimer ??= setTimeout(() => killGroup('SIGKILL'), 250);
91
91
  killTimer.unref();
92
92
  };
93
93
  const onAbort = () => {
94
+ if (aborted)
95
+ return;
94
96
  aborted = true;
95
97
  terminate();
96
98
  };
@@ -98,9 +100,6 @@ export function spawnOpenCode(executable, args, options) {
98
100
  overflow = true;
99
101
  terminate();
100
102
  };
101
- options.signal?.addEventListener('abort', onAbort, { once: true });
102
- if (options.signal?.aborted)
103
- onAbort();
104
103
  child.stdout.on('data', (chunk) => {
105
104
  stdoutBytes += chunk.length;
106
105
  if (stdoutBytes <= cap)
@@ -114,16 +113,23 @@ export function spawnOpenCode(executable, args, options) {
114
113
  onOverflow();
115
114
  });
116
115
  child.stdin.on('error', () => undefined);
116
+ child.once('error', (error) => finish(undefined, undefined, error));
117
+ child.once('close', (code, signal) => finish(code, signal));
118
+ options.signal?.addEventListener('abort', onAbort, { once: true });
119
+ if (options.signal?.aborted)
120
+ onAbort();
117
121
  if (!aborted)
118
122
  child.stdin.end(options.stdin);
119
123
  else
120
124
  child.stdin.destroy();
121
- child.once('error', (error) => finish(undefined, undefined, error));
122
- child.once('close', (code, signal) => finish(code, signal));
123
125
  function finish(code, signal, error) {
124
126
  if (settled)
125
127
  return;
126
128
  settled = true;
129
+ if (child.pid && terminating)
130
+ killOpenCodeProcessGroup(child.pid, 'SIGKILL');
131
+ if (child.pid)
132
+ unregisterOpenCodeProcessGroup(child.pid);
127
133
  options.signal?.removeEventListener('abort', onAbort);
128
134
  if (killTimer)
129
135
  clearTimeout(killTimer);
@@ -377,6 +383,7 @@ class OpenCodeSession {
377
383
  disposed = false;
378
384
  sessionId;
379
385
  inFlight;
386
+ disposeController = new AbortController();
380
387
  constructor(runtime, options) {
381
388
  this.workspacePath = getWorkspacePath(runtime);
382
389
  this.runtimeEnv = getRuntimeEnv(runtime);
@@ -432,11 +439,15 @@ class OpenCodeSession {
432
439
  '--model', OPENCODE_MODEL, '--agent', 'build',
433
440
  ...(this.sessionId ? ['--session', this.sessionId] : []),
434
441
  ];
442
+ const turnSignal = this.getAbortSignal();
443
+ const signal = turnSignal
444
+ ? AbortSignal.any([turnSignal, this.disposeController.signal])
445
+ : this.disposeController.signal;
435
446
  const processResult = await spawnOpenCode(this.resolvedExecutable, args, {
436
447
  cwd: this.workspacePath,
437
448
  env,
438
449
  stdin: message,
439
- signal: this.getAbortSignal(),
450
+ signal,
440
451
  });
441
452
  const parsed = parseOpenCodeOutput(processResult.stdout, processResult, this.mcpToolNames);
442
453
  if (this.sessionId && parsed.sessionId !== this.sessionId) {
@@ -494,6 +505,7 @@ class OpenCodeSession {
494
505
  if (this.disposed)
495
506
  return;
496
507
  this.disposed = true;
508
+ this.disposeController.abort();
497
509
  await this.inFlight?.catch(() => undefined);
498
510
  await this.cleanupState();
499
511
  }
package/dist/sdk/agent.js CHANGED
@@ -15,7 +15,7 @@ import { createVerboseEmitter } from '../reporters/verbose-emitter.js';
15
15
  import fs from 'fs-extra';
16
16
  import * as path from 'path';
17
17
  import { cleanDebugRuns, DEFAULT_DEBUG_RETAIN_RUNS, prepareManagedDebugRun, } from '../providers/debug-runs.js';
18
- import { collectOpenCodeMcpToolNames, validateOpenCodeDeclaration } from '../agents/opencode-contract.js';
18
+ import { collectOpenCodeMcpToolNames, validateOpenCodeDeclaration } from '../agents/opencode/contract.js';
19
19
  /**
20
20
  * Test-only injection point: override the sink used by the next emitter
21
21
  * built inside `createAgent`. Pass `null` to restore the default (stderr).
@@ -35,13 +35,14 @@ export async function runJudgeSession(input, options = {}) {
35
35
  { role: 'user', content: user },
36
36
  ];
37
37
  let rounds = 0;
38
+ let scoreRepairAttempted = false;
38
39
  while (rounds < maxRounds) {
39
40
  rounds++;
40
41
  let response;
41
42
  try {
42
43
  response = await llm.callWithTools(messages, {
43
44
  system,
44
- tools: toolSchemas,
45
+ tools: scoreRepairAttempted ? [] : toolSchemas,
45
46
  model: scorer.model,
46
47
  cacheControl: scorer.cacheControl,
47
48
  });
@@ -63,6 +64,22 @@ export async function runJudgeSession(input, options = {}) {
63
64
  }
64
65
  const parsed = parseFinalScore(response.text);
65
66
  if (!parsed.ok) {
67
+ if (parsed.message.startsWith('JSON parse failed:')
68
+ && !scoreRepairAttempted
69
+ && rounds < maxRounds) {
70
+ scoreRepairAttempted = true;
71
+ messages.push({ role: 'assistant', content: response.text });
72
+ messages.push({
73
+ role: 'user',
74
+ content: [
75
+ `Your final answer was not valid JSON: ${parsed.message}.`,
76
+ 'Return ONLY a valid JSON object with double-quoted keys in this exact shape:',
77
+ '{"score": <number 0..1>, "details": "<brief explanation>"}',
78
+ 'Do not call more tools or include Markdown fences.',
79
+ ].join('\n'),
80
+ });
81
+ continue;
82
+ }
66
83
  return makeOutcome(tokenUsage, toolCalls, logEntries, rounds, {
67
84
  code: 'invalid_score',
68
85
  details: parsed.message,
@@ -11,6 +11,11 @@ function resolveBaseUrl(env) {
11
11
  || process.env.APP_ANTHROPIC_BASE_URL
12
12
  || 'https://api.anthropic.com';
13
13
  }
14
+ function resolveMessagesUrl(env) {
15
+ const baseUrl = resolveBaseUrl(env).replace(/\/+$/, '');
16
+ const apiRoot = baseUrl.endsWith('/v1') ? baseUrl : `${baseUrl}/v1`;
17
+ return `${apiRoot}/messages`;
18
+ }
14
19
  function buildHeaders(apiKey, useCache) {
15
20
  const headers = {
16
21
  'Content-Type': 'application/json',
@@ -57,7 +62,7 @@ function resolveTemperature(model, config, temperature) {
57
62
  return temperature ?? 0;
58
63
  }
59
64
  async function postAnthropic(apiKey, useCache, body, env) {
60
- const response = await fetch(`${resolveBaseUrl(env)}/v1/messages`, {
65
+ const response = await fetch(resolveMessagesUrl(env), {
61
66
  method: 'POST',
62
67
  headers: buildHeaders(apiKey, useCache),
63
68
  body: JSON.stringify(body),
package/dist/utils/llm.js CHANGED
@@ -199,6 +199,7 @@ export function createAgentLLM(agentName, agentEnv) {
199
199
  const adapters = agentEnv && Object.keys(agentEnv).length > 0
200
200
  ? baseAdapters.map((a) => ({
201
201
  ...a,
202
+ isAvailable: (env) => a.isAvailable({ ...agentEnv, ...env }),
202
203
  call: (prompt, opts) => a.call(prompt, { ...opts, env: { ...agentEnv, ...opts.env } }),
203
204
  ...(a.callWithTools
204
205
  ? { callWithTools: (messages, opts) => a.callWithTools(messages, { ...opts, env: { ...agentEnv, ...opts.env } }) }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/pathgrade",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "packageManager": "yarn@4.12.0",
5
5
  "description": "Evaluate whether AI agents discover and use your skills correctly",
6
6
  "exports": {
@@ -132,5 +132,5 @@
132
132
  "typescript": "^5.9.3",
133
133
  "zod": "4.3.6"
134
134
  },
135
- "falconPackageHash": "130e5449f0131fe2bba40bf0237a7620315714dfe492f7e5e9d7cc14"
135
+ "falconPackageHash": "be877aa61dfb2cd1b58c716b704cffee9592428675a660382e86e358"
136
136
  }