@wix/pathgrade 1.0.24 → 1.0.26
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 +42 -0
- package/dist/agents/claude/sdk-options.d.ts +1 -2
- package/dist/agents/claude.js +2 -1
- package/dist/agents/codex-app-server/agent.js +83 -46
- package/dist/agents/codex-app-server/scenario-mount.d.ts +40 -0
- package/dist/agents/codex-app-server/scenario-mount.js +238 -0
- package/dist/agents/cursor-scenario.d.ts +20 -0
- package/dist/agents/cursor-scenario.js +96 -0
- package/dist/agents/cursor.js +20 -0
- package/dist/agents/opencode/scenario.d.ts +25 -0
- package/dist/agents/opencode/scenario.js +195 -0
- package/dist/agents/opencode.js +50 -0
- package/dist/internal/direct-mcp-v2/acp-author-projector.d.ts +20 -0
- package/dist/internal/direct-mcp-v2/acp-author-projector.js +147 -0
- package/dist/internal/direct-mcp-v2/acp-author-session.d.ts +39 -0
- package/dist/internal/direct-mcp-v2/acp-author-session.js +201 -0
- package/dist/internal/direct-mcp-v2/acp-client.d.ts +70 -0
- package/dist/internal/direct-mcp-v2/acp-client.js +310 -0
- package/dist/internal/direct-mcp-v2/acp-direct-mcp.d.ts +38 -0
- package/dist/internal/direct-mcp-v2/acp-direct-mcp.js +326 -0
- package/dist/internal/direct-mcp-v2/claude-direct-mcp.d.ts +68 -0
- package/dist/internal/direct-mcp-v2/claude-direct-mcp.js +456 -0
- package/dist/internal/direct-mcp-v2/claude-profile.d.ts +9 -0
- package/dist/internal/direct-mcp-v2/claude-profile.js +97 -0
- package/dist/internal/direct-mcp-v2/cleanup.d.ts +62 -0
- package/dist/internal/direct-mcp-v2/cleanup.js +182 -0
- package/dist/internal/direct-mcp-v2/codex-direct-mcp.d.ts +41 -0
- package/dist/internal/direct-mcp-v2/codex-direct-mcp.js +436 -0
- package/dist/internal/direct-mcp-v2/codex-profile.d.ts +12 -0
- package/dist/internal/direct-mcp-v2/codex-profile.js +81 -0
- package/dist/internal/direct-mcp-v2/compiler.d.ts +12 -0
- package/dist/internal/direct-mcp-v2/compiler.js +449 -0
- package/dist/internal/direct-mcp-v2/cursor-direct-mcp.d.ts +14 -0
- package/dist/internal/direct-mcp-v2/cursor-direct-mcp.js +60 -0
- package/dist/internal/direct-mcp-v2/cursor-profile.d.ts +38 -0
- package/dist/internal/direct-mcp-v2/cursor-profile.js +79 -0
- package/dist/internal/direct-mcp-v2/evidence.d.ts +50 -0
- package/dist/internal/direct-mcp-v2/evidence.js +147 -0
- package/dist/internal/direct-mcp-v2/host-reducer.d.ts +61 -0
- package/dist/internal/direct-mcp-v2/host-reducer.js +214 -0
- package/dist/internal/direct-mcp-v2/json.d.ts +9 -0
- package/dist/internal/direct-mcp-v2/json.js +220 -0
- package/dist/internal/direct-mcp-v2/materialize.d.ts +39 -0
- package/dist/internal/direct-mcp-v2/materialize.js +22 -0
- package/dist/internal/direct-mcp-v2/opencode-direct-mcp.d.ts +14 -0
- package/dist/internal/direct-mcp-v2/opencode-direct-mcp.js +71 -0
- package/dist/internal/direct-mcp-v2/opencode-profile.d.ts +8 -0
- package/dist/internal/direct-mcp-v2/opencode-profile.js +52 -0
- package/dist/internal/direct-mcp-v2/projection.d.ts +40 -0
- package/dist/internal/direct-mcp-v2/projection.js +46 -0
- package/dist/internal/direct-mcp-v2/public-scenario-runtime.d.ts +51 -0
- package/dist/internal/direct-mcp-v2/public-scenario-runtime.js +353 -0
- package/dist/internal/direct-mcp-v2/runtime-lock.d.ts +51 -0
- package/dist/internal/direct-mcp-v2/runtime-lock.js +203 -0
- package/dist/internal/direct-mcp-v2/runtime-profile.d.ts +28 -0
- package/dist/internal/direct-mcp-v2/runtime-profile.js +163 -0
- package/dist/internal/direct-mcp-v2/scenario-http-host.d.ts +24 -0
- package/dist/internal/direct-mcp-v2/scenario-http-host.js +254 -0
- package/dist/internal/direct-mcp-v2/types.d.ts +233 -0
- package/dist/internal/direct-mcp-v2/types.js +1 -0
- package/dist/sdk/agent.js +57 -2
- package/dist/sdk/evaluate.js +6 -3
- package/dist/sdk/index.d.ts +4 -0
- package/dist/sdk/index.js +2 -0
- package/dist/sdk/managed-session.d.ts +6 -0
- package/dist/sdk/managed-session.js +157 -50
- package/dist/sdk/scenario-evidence.d.ts +27 -0
- package/dist/sdk/scenario-evidence.js +94 -0
- package/dist/sdk/scenario-machine-v2.d.ts +9 -0
- package/dist/sdk/scenario-machine-v2.js +37 -0
- package/dist/sdk/snapshots.d.ts +3 -0
- package/dist/sdk/snapshots.js +2 -0
- package/dist/sdk/types.d.ts +9 -1
- package/dist/types.d.ts +6 -0
- package/package.json +2 -2
package/dist/sdk/agent.js
CHANGED
|
@@ -19,6 +19,11 @@ import { cleanDebugRuns, DEFAULT_DEBUG_RETAIN_RUNS, prepareManagedDebugRun, } fr
|
|
|
19
19
|
import { collectOpenCodeMcpToolNames, validateOpenCodeDeclaration } from '../agents/opencode/contract.js';
|
|
20
20
|
import { collectSensitiveEnvValues } from '../tool-event-results.js';
|
|
21
21
|
import { compileMcpMockApprovalSession, } from './mcp-mock-approvals.js';
|
|
22
|
+
import { compileScenario } from './scenario-machine-v2.js';
|
|
23
|
+
import { startPublicCodexScenarioRuntime } from '../agents/codex-app-server/scenario-mount.js';
|
|
24
|
+
import { createPublicOpenCodeScenarioRuntimeFactory } from '../agents/opencode/scenario.js';
|
|
25
|
+
import { createPublicCursorScenarioRuntimeFactory } from '../agents/cursor-scenario.js';
|
|
26
|
+
import { createAskUserHandler } from './ask-bus/handler.js';
|
|
22
27
|
/**
|
|
23
28
|
* Test-only injection point: override the sink used by the next emitter
|
|
24
29
|
* built inside `createAgent`. Pass `null` to restore the default (stderr).
|
|
@@ -51,6 +56,9 @@ class AgentImpl {
|
|
|
51
56
|
activeChatSession;
|
|
52
57
|
scriptedMcp;
|
|
53
58
|
sensitiveValues;
|
|
59
|
+
scenarioArtifact;
|
|
60
|
+
scenarioRuntimeFactory;
|
|
61
|
+
_scenarioEvidence = [];
|
|
54
62
|
constructor(opts) {
|
|
55
63
|
this.ws = opts.workspace;
|
|
56
64
|
this.agentName = opts.agentName;
|
|
@@ -68,6 +76,8 @@ class AgentImpl {
|
|
|
68
76
|
this.opencodeMcpToolNames = opts.opencodeMcpToolNames;
|
|
69
77
|
this.scriptedMcp = opts.scriptedMcp;
|
|
70
78
|
this.sensitiveValues = opts.sensitiveValues;
|
|
79
|
+
this.scenarioArtifact = opts.scenarioArtifact;
|
|
80
|
+
this.scenarioRuntimeFactory = opts.scenarioRuntimeFactory;
|
|
71
81
|
}
|
|
72
82
|
get messages() {
|
|
73
83
|
return this._messages;
|
|
@@ -78,6 +88,9 @@ class AgentImpl {
|
|
|
78
88
|
get workspace() {
|
|
79
89
|
return this.ws.path;
|
|
80
90
|
}
|
|
91
|
+
get scenarioEvidence() {
|
|
92
|
+
return structuredClone(this._scenarioEvidence);
|
|
93
|
+
}
|
|
81
94
|
get executionMetadata() {
|
|
82
95
|
const interactionMode = this.interactionMode === 'startChat'
|
|
83
96
|
? 'start_chat'
|
|
@@ -87,7 +100,9 @@ class AgentImpl {
|
|
|
87
100
|
return {
|
|
88
101
|
name: this.agentName,
|
|
89
102
|
...(this.modelOpt ? { requestedModel: this.modelOpt } : {}),
|
|
90
|
-
transport:
|
|
103
|
+
transport: this.scenarioArtifact && (this.agentName === 'opencode' || this.agentName === 'cursor')
|
|
104
|
+
? 'acp'
|
|
105
|
+
: resolveExecutionTransport(this.agentName, this.transport),
|
|
91
106
|
...(interactionMode ? { interactionMode } : {}),
|
|
92
107
|
};
|
|
93
108
|
}
|
|
@@ -113,6 +128,9 @@ class AgentImpl {
|
|
|
113
128
|
...(this.opencodeExecutable !== undefined ? { opencodeExecutable: this.opencodeExecutable } : {}),
|
|
114
129
|
...(this.opencodeMcpToolNames !== undefined ? { opencodeMcpToolNames: this.opencodeMcpToolNames } : {}),
|
|
115
130
|
...(this.scriptedMcp !== undefined ? { scriptedMcp: this.scriptedMcp } : {}),
|
|
131
|
+
...(this.scenarioArtifact !== undefined ? { scenarioArtifact: this.scenarioArtifact } : {}),
|
|
132
|
+
...(this.scenarioRuntimeFactory !== undefined ? { scenarioRuntimeFactory: this.scenarioRuntimeFactory } : {}),
|
|
133
|
+
onScenarioEvidence: (evidence) => { this._scenarioEvidence = structuredClone(evidence); },
|
|
116
134
|
sensitiveValues: this.sensitiveValues,
|
|
117
135
|
});
|
|
118
136
|
}
|
|
@@ -196,10 +214,17 @@ class AgentImpl {
|
|
|
196
214
|
throw new Error('prompt() can only be called once per agent');
|
|
197
215
|
}
|
|
198
216
|
const ms = this.createSession(this.resolveTimeoutSec('prompt'));
|
|
217
|
+
const askUser = createAskUserHandler({
|
|
218
|
+
reactions: [],
|
|
219
|
+
onUnmatchedAskUser: 'decline',
|
|
220
|
+
firedOnce: new Set(),
|
|
221
|
+
});
|
|
222
|
+
const unsubscribeAskUser = ms.askBus.onAsk(askUser.handler);
|
|
199
223
|
try {
|
|
200
224
|
return await this.executeLoggedTurn(ms, message, 1, 'agent_start');
|
|
201
225
|
}
|
|
202
226
|
finally {
|
|
227
|
+
unsubscribeAskUser();
|
|
203
228
|
await ms.dispose?.();
|
|
204
229
|
}
|
|
205
230
|
}
|
|
@@ -354,6 +379,7 @@ class AgentImpl {
|
|
|
354
379
|
conversationResult: this.lastConversationResult,
|
|
355
380
|
workspace: dest,
|
|
356
381
|
sensitiveValues: this.sensitiveValues,
|
|
382
|
+
scenarioEvidence: this._scenarioEvidence,
|
|
357
383
|
});
|
|
358
384
|
await fs.writeJSON(path.join(dest, 'run-snapshot.json'), snapshot, { spaces: 2 });
|
|
359
385
|
}
|
|
@@ -401,10 +427,37 @@ export async function createAgent(opts) {
|
|
|
401
427
|
}
|
|
402
428
|
}
|
|
403
429
|
const agentName = resolveAgentName(opts, process.env);
|
|
430
|
+
let scenarioArtifact;
|
|
431
|
+
if (opts.mcpScenario !== undefined) {
|
|
432
|
+
if (opts.agent === undefined)
|
|
433
|
+
throw new Error('mcpScenario requires an explicit agent');
|
|
434
|
+
if (opts.mcpMock !== undefined || opts.mcpMockApprovalRules !== undefined || opts.mcpConfigFile !== undefined || opts.mcpSafety !== undefined) {
|
|
435
|
+
throw new Error('mcpScenario cannot be combined with legacy or live MCP options');
|
|
436
|
+
}
|
|
437
|
+
const compiled = compileScenario(opts.mcpScenario);
|
|
438
|
+
if (!compiled.ok)
|
|
439
|
+
throw new Error(`Scenario compilation failed: ${JSON.stringify(compiled.diagnostics)}`);
|
|
440
|
+
scenarioArtifact = compiled.artifact;
|
|
441
|
+
}
|
|
404
442
|
validateOpenCodeDeclaration(agentName, opts);
|
|
405
443
|
const transport = agentName === 'codex'
|
|
406
444
|
? resolveCodexTransport(opts, process.env)
|
|
407
445
|
: undefined;
|
|
446
|
+
if (scenarioArtifact && agentName === 'codex' && transport !== 'app-server') {
|
|
447
|
+
throw new Error('mcpScenario requires Codex transport app-server; Codex exec is unsupported');
|
|
448
|
+
}
|
|
449
|
+
const scenarioRuntimeFactory = !scenarioArtifact
|
|
450
|
+
? undefined
|
|
451
|
+
: agentName === 'codex'
|
|
452
|
+
? startPublicCodexScenarioRuntime
|
|
453
|
+
: agentName === 'opencode'
|
|
454
|
+
? createPublicOpenCodeScenarioRuntimeFactory({
|
|
455
|
+
opencodeExecutable: opts.opencodeExecutable,
|
|
456
|
+
requestedModel: opts.model,
|
|
457
|
+
})
|
|
458
|
+
: agentName === 'cursor'
|
|
459
|
+
? createPublicCursorScenarioRuntimeFactory({ requestedModel: opts.model })
|
|
460
|
+
: undefined;
|
|
408
461
|
let scriptedMcp;
|
|
409
462
|
if (opts.mcpMockApprovalRules !== undefined) {
|
|
410
463
|
if (opts.mcpMock === undefined)
|
|
@@ -426,7 +479,7 @@ export async function createAgent(opts) {
|
|
|
426
479
|
const timeoutSetting = opts.timeout ?? 300;
|
|
427
480
|
// Capture runner context now; adapters own installation and restoration.
|
|
428
481
|
const testCtx = opts.debug ? resolveCaseDebugContext() : { name: '', dir: '' };
|
|
429
|
-
const { timeout: _, mcpMock, mcpConfigFile, mcpMockApprovalRules: ________, agent: __, debug: ___, transport: _____, mcpSafety: ______, opencodeExecutable, ...rest } = opts;
|
|
482
|
+
const { timeout: _, mcpMock, mcpScenario: _________, mcpConfigFile, mcpMockApprovalRules: ________, agent: __, debug: ___, transport: _____, mcpSafety: ______, opencodeExecutable, ...rest } = opts;
|
|
430
483
|
const workspace = await prepareWorkspace({
|
|
431
484
|
...rest,
|
|
432
485
|
agent: agentName,
|
|
@@ -463,6 +516,8 @@ export async function createAgent(opts) {
|
|
|
463
516
|
opencodeExecutable,
|
|
464
517
|
opencodeMcpToolNames: agentName === 'opencode' ? collectOpenCodeMcpToolNames(mcpMock) : undefined,
|
|
465
518
|
scriptedMcp,
|
|
519
|
+
scenarioArtifact,
|
|
520
|
+
scenarioRuntimeFactory,
|
|
466
521
|
sensitiveValues: workspace.sensitiveValues ?? collectSensitiveEnvValues(workspace.env),
|
|
467
522
|
});
|
|
468
523
|
lifecycleCore.registerAgent(agent);
|
package/dist/sdk/evaluate.js
CHANGED
|
@@ -42,6 +42,7 @@ function makeEvaluateAgent() {
|
|
|
42
42
|
toolEvents,
|
|
43
43
|
runCommand: (cmd) => agent.exec(cmd),
|
|
44
44
|
artifacts: createSessionArtifacts(agent.workspace, toolEvents),
|
|
45
|
+
scenarioEvidence: agent.scenarioEvidence,
|
|
45
46
|
};
|
|
46
47
|
const trackedLLM = opts?.llm ?? agent.llm;
|
|
47
48
|
// Snapshot conversation tokens BEFORE running scorers, for first-eval attribution.
|
|
@@ -80,7 +81,7 @@ function makeEvaluateAgent() {
|
|
|
80
81
|
...(opts?.evaluationDefinitionKey
|
|
81
82
|
? { evaluationDefinitionKey: opts.evaluationDefinitionKey }
|
|
82
83
|
: {}),
|
|
83
|
-
trial: buildTrialResult(agent.log, { ...evalResult, tokenUsage: deltaTokenUsage }, conversationTokens, conversationCost),
|
|
84
|
+
trial: buildTrialResult(agent.log, { ...evalResult, tokenUsage: deltaTokenUsage }, agent.scenarioEvidence, conversationTokens, conversationCost),
|
|
84
85
|
};
|
|
85
86
|
emitEvalResult({ result: recordedResult, agent });
|
|
86
87
|
maybeThrowOnScorerErrors(recordedResult, opts?.onScorerError ?? 'skip');
|
|
@@ -110,6 +111,7 @@ async function fromSnapshot(snapshotPath, scorers, opts) {
|
|
|
110
111
|
return sandboxExec(cmd, { cwd: workspace, env: getProcessEnv() });
|
|
111
112
|
},
|
|
112
113
|
artifacts: createSessionArtifacts(snapshot.workspace ?? '', snapshot.toolEvents),
|
|
114
|
+
scenarioEvidence: snapshot.scenarioEvidence ?? [],
|
|
113
115
|
};
|
|
114
116
|
const scoringStartedAt = performance.now();
|
|
115
117
|
const evalResult = await evaluateWithContext(ctx, scorers, { ...opts, llm: trackedLLM });
|
|
@@ -121,7 +123,7 @@ async function fromSnapshot(snapshotPath, scorers, opts) {
|
|
|
121
123
|
...(opts?.evaluationDefinitionKey
|
|
122
124
|
? { evaluationDefinitionKey: opts.evaluationDefinitionKey }
|
|
123
125
|
: {}),
|
|
124
|
-
trial: buildTrialResult(snapshot.log, evalResult),
|
|
126
|
+
trial: buildTrialResult(snapshot.log, evalResult, snapshot.scenarioEvidence),
|
|
125
127
|
};
|
|
126
128
|
maybeThrowOnScorerErrors(recordedResult, opts?.onScorerError ?? 'skip');
|
|
127
129
|
return recordedResult;
|
|
@@ -265,7 +267,7 @@ function matchesArtifactPattern(artifactPath, pattern) {
|
|
|
265
267
|
return artifactPath.includes(pattern);
|
|
266
268
|
return pattern.test(artifactPath);
|
|
267
269
|
}
|
|
268
|
-
function buildTrialResult(log, result, conversationTokens, conversationCost) {
|
|
270
|
+
function buildTrialResult(log, result, scenarioEvidence, conversationTokens, conversationCost) {
|
|
269
271
|
const nCommands = log.filter((entry) => entry.type === 'command').length;
|
|
270
272
|
const skills = extractSkillsFromLog(log);
|
|
271
273
|
return {
|
|
@@ -285,6 +287,7 @@ function buildTrialResult(log, result, conversationTokens, conversationCost) {
|
|
|
285
287
|
// field.
|
|
286
288
|
...conversationCost,
|
|
287
289
|
session_log: [...log],
|
|
290
|
+
...(scenarioEvidence && scenarioEvidence.length > 0 ? { scenario_evidence: structuredClone(scenarioEvidence) } : {}),
|
|
288
291
|
...(skills.length > 0 ? { skills_used: skills } : {}),
|
|
289
292
|
};
|
|
290
293
|
}
|
package/dist/sdk/index.d.ts
CHANGED
|
@@ -2,8 +2,12 @@ export { createAgent } from './agent.js';
|
|
|
2
2
|
export { resolveAgentName, resolveCodexTransport, InvalidTransportEnvError, } from './agent-resolution.js';
|
|
3
3
|
export { AgentCrashError } from './agent-crash.js';
|
|
4
4
|
export { check, score, judge, toolUsage } from './scorers.js';
|
|
5
|
+
export { compileScenario, compileScenarioText } from './scenario-machine-v2.js';
|
|
6
|
+
export { findScenarioCaseMatches, getFinalScenarioState, getScenarioStateTimeline, ScenarioEvidenceError, wasScenarioCaseMatched, } from './scenario-evidence.js';
|
|
5
7
|
export { getMcpToolCall, isMcpToolCall, findMcpToolCalls, getMcpStartupStatus, isMcpStartupStatus, getMcpApproval, isMcpApproval, findMcpApprovals, getMcpInvocation, wasMcpToolInvoked, } from './mcp-evidence.js';
|
|
6
8
|
export type { McpMockApprovalRule, McpMockJsonValue } from './mcp-mock-approvals.js';
|
|
9
|
+
export type { CompileScenarioResult, EvidenceEnvelope, JsonObject, JsonPrimitive, JsonValue, MatchExpr, MatchPredicate, McpContent, ScenarioArtifact, ScenarioCase, ScenarioDiagnostic, ScenarioEvidence, ScenarioMachineV2, ScenarioRejection, ScenarioSuccess, ScenarioToolError, } from './scenario-machine-v2.js';
|
|
10
|
+
export type { ExpectedScenarioCaseMatch, ScenarioCaseMatchEvidence, ScenarioStateTransitionEvidence, } from './scenario-evidence.js';
|
|
7
11
|
export { decideMcpToolCall, redactMcpSecrets, } from './mcp-safety.js';
|
|
8
12
|
export { evaluate, EvalScorerError } from './evaluate.js';
|
|
9
13
|
export { RUN_SNAPSHOT_VERSION, buildRunSnapshot, loadRunSnapshot, SnapshotParseError, SnapshotVersionError, WorkspaceMissingError, } from './snapshots.js';
|
package/dist/sdk/index.js
CHANGED
|
@@ -3,6 +3,8 @@ export { createAgent } from './agent.js';
|
|
|
3
3
|
export { resolveAgentName, resolveCodexTransport, InvalidTransportEnvError, } from './agent-resolution.js';
|
|
4
4
|
export { AgentCrashError } from './agent-crash.js';
|
|
5
5
|
export { check, score, judge, toolUsage } from './scorers.js';
|
|
6
|
+
export { compileScenario, compileScenarioText } from './scenario-machine-v2.js';
|
|
7
|
+
export { findScenarioCaseMatches, getFinalScenarioState, getScenarioStateTimeline, ScenarioEvidenceError, wasScenarioCaseMatched, } from './scenario-evidence.js';
|
|
6
8
|
export { getMcpToolCall, isMcpToolCall, findMcpToolCalls, getMcpStartupStatus, isMcpStartupStatus, getMcpApproval, isMcpApproval, findMcpApprovals, getMcpInvocation, wasMcpToolInvoked, } from './mcp-evidence.js';
|
|
7
9
|
export { decideMcpToolCall, redactMcpSecrets, } from './mcp-safety.js';
|
|
8
10
|
export { evaluate, EvalScorerError } from './evaluate.js';
|
|
@@ -5,6 +5,8 @@ import type { McpSafetyOptions } from './mcp-safety.js';
|
|
|
5
5
|
import type { LLMPort } from '../utils/llm-types.js';
|
|
6
6
|
import type { AskBus } from './ask-bus/types.js';
|
|
7
7
|
import type { CompiledMcpMockSession } from './mcp-mock-approvals.js';
|
|
8
|
+
import { type PublicScenarioRuntimeFactory } from '../internal/direct-mcp-v2/public-scenario-runtime.js';
|
|
9
|
+
import type { EvidenceEnvelope, ScenarioArtifact } from '../internal/direct-mcp-v2/types.js';
|
|
8
10
|
export interface ManagedSessionDeps {
|
|
9
11
|
ws: Workspace;
|
|
10
12
|
agentName: AgentName;
|
|
@@ -36,6 +38,10 @@ export interface ManagedSessionDeps {
|
|
|
36
38
|
opencodeMcpToolNames?: string[];
|
|
37
39
|
/** Trusted pre-workspace compiled generated-MCP declaration. */
|
|
38
40
|
scriptedMcp?: CompiledMcpMockSession;
|
|
41
|
+
scenarioArtifact?: ScenarioArtifact;
|
|
42
|
+
/** Provider adapter entrypoint; defaults to the preserved Claude bridge. */
|
|
43
|
+
scenarioRuntimeFactory?: PublicScenarioRuntimeFactory;
|
|
44
|
+
onScenarioEvidence?: (evidence: readonly EvidenceEnvelope[]) => void;
|
|
39
45
|
/** Runtime-only values that must not enter session logs or persisted results. */
|
|
40
46
|
sensitiveValues?: readonly string[];
|
|
41
47
|
}
|
|
@@ -10,6 +10,8 @@ import { attachTurnResultSensitiveValues, cloneTurnResultWithSensitiveValues, ge
|
|
|
10
10
|
import { attachToolEventSensitiveValues, getToolEventSensitiveValues } from './tool-event-secrets.js';
|
|
11
11
|
import { sanitizePersistenceValue } from '../tool-event-results.js';
|
|
12
12
|
import { startScriptedMcpMockHost, } from '../providers/scripted-mcp-mock-host.js';
|
|
13
|
+
import { startPublicClaudeScenarioRuntime, } from '../internal/direct-mcp-v2/public-scenario-runtime.js';
|
|
14
|
+
import { TrialExecutionError } from '../internal/direct-mcp-v2/cleanup.js';
|
|
13
15
|
export function createManagedSession(deps) {
|
|
14
16
|
const { ws, agentName, timeoutSec, messages, log, model, conversationWindow, llm } = deps;
|
|
15
17
|
const agentTimeoutMs = timeoutSec * 1000;
|
|
@@ -40,9 +42,12 @@ export function createManagedSession(deps) {
|
|
|
40
42
|
let setupDone = false;
|
|
41
43
|
let currentSignal;
|
|
42
44
|
let scriptedHost;
|
|
45
|
+
let scenarioRuntime;
|
|
43
46
|
let activeTurn = false;
|
|
44
47
|
let nextTurnNumber = 0;
|
|
45
48
|
let disposePromise;
|
|
49
|
+
let primaryTurnError;
|
|
50
|
+
let inFlightTurn;
|
|
46
51
|
const runCommand = async (cmd) => {
|
|
47
52
|
const result = await ws.exec(cmd, { signal: currentSignal });
|
|
48
53
|
log.push(sanitizePersistenceValue({
|
|
@@ -62,67 +67,145 @@ export function createManagedSession(deps) {
|
|
|
62
67
|
if (remaining <= 0)
|
|
63
68
|
throw new Error(`${label} timed out`);
|
|
64
69
|
activeTurn = true;
|
|
70
|
+
let finishTurnJoin;
|
|
71
|
+
const turnJoin = new Promise((resolve) => { finishTurnJoin = resolve; });
|
|
72
|
+
inFlightTurn = turnJoin;
|
|
65
73
|
try {
|
|
66
74
|
return await withAbortTimeout(async (signal) => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
try {
|
|
76
|
+
currentSignal = signal;
|
|
77
|
+
if (!session) {
|
|
78
|
+
// Run auth setup commands before first agent turn
|
|
79
|
+
if (!setupDone) {
|
|
80
|
+
for (const cmd of ws.setupCommands) {
|
|
81
|
+
await ws.exec(cmd, { signal });
|
|
82
|
+
signal.throwIfAborted();
|
|
83
|
+
}
|
|
84
|
+
setupDone = true;
|
|
85
|
+
}
|
|
86
|
+
// Carry the workspace's resolved env (from `prepareWorkspace
|
|
87
|
+
// → resolveCredentials`) into the runtime handle so drivers
|
|
88
|
+
// that auth through `Options.env` (notably the Claude SDK
|
|
89
|
+
// driver) can lift Anthropic keys out. Drivers that only
|
|
90
|
+
// need the workspace path keep using `getWorkspacePath`.
|
|
91
|
+
const runtime = {
|
|
92
|
+
handle: ws.path,
|
|
93
|
+
workspacePath: ws.path,
|
|
94
|
+
env: ws.env,
|
|
95
|
+
};
|
|
96
|
+
if (deps.scriptedMcp) {
|
|
97
|
+
scriptedHost = await startScriptedMcpMockHost(deps.scriptedMcp);
|
|
98
|
+
sessionOptions.scriptedMcpHost = scriptedHost;
|
|
99
|
+
}
|
|
100
|
+
if (deps.scenarioArtifact) {
|
|
101
|
+
const startScenarioRuntime = deps.scenarioRuntimeFactory ?? startPublicClaudeScenarioRuntime;
|
|
102
|
+
scenarioRuntime = await startScenarioRuntime({
|
|
103
|
+
artifact: deps.scenarioArtifact,
|
|
104
|
+
workspacePath: ws.path,
|
|
105
|
+
environment: ws.env,
|
|
106
|
+
onEvidence: deps.onScenarioEvidence,
|
|
107
|
+
});
|
|
108
|
+
try {
|
|
109
|
+
scenarioRuntime.configureSession(sessionOptions);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
try {
|
|
113
|
+
await scenarioRuntime.dispose();
|
|
114
|
+
}
|
|
115
|
+
catch (cleanupError) {
|
|
116
|
+
throw new AggregateError([error, cleanupError], 'Scenario provider projection failed and coordinator cleanup also failed');
|
|
117
|
+
}
|
|
118
|
+
scenarioRuntime = undefined;
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
session = await createAgentSession(agent, runtime, runCommand, sessionOptions);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
try {
|
|
127
|
+
await scriptedHost?.dispose();
|
|
128
|
+
await scenarioRuntime?.dispose();
|
|
129
|
+
}
|
|
130
|
+
catch (cleanupError) {
|
|
131
|
+
throw new AggregateError([error, cleanupError], 'Agent session setup failed and scenario cleanup also failed');
|
|
132
|
+
}
|
|
133
|
+
scriptedHost = undefined;
|
|
134
|
+
scenarioRuntime = undefined;
|
|
135
|
+
throw error;
|
|
74
136
|
}
|
|
75
|
-
setupDone = true;
|
|
76
|
-
}
|
|
77
|
-
// Carry the workspace's resolved env (from `prepareWorkspace
|
|
78
|
-
// → resolveCredentials`) into the runtime handle so drivers
|
|
79
|
-
// that auth through `Options.env` (notably the Claude SDK
|
|
80
|
-
// driver) can lift Anthropic keys out. Drivers that only
|
|
81
|
-
// need the workspace path keep using `getWorkspacePath`.
|
|
82
|
-
const runtime = {
|
|
83
|
-
handle: ws.path,
|
|
84
|
-
workspacePath: ws.path,
|
|
85
|
-
env: ws.env,
|
|
86
|
-
};
|
|
87
|
-
if (deps.scriptedMcp) {
|
|
88
|
-
scriptedHost = await startScriptedMcpMockHost(deps.scriptedMcp);
|
|
89
|
-
sessionOptions.scriptedMcpHost = scriptedHost;
|
|
90
137
|
}
|
|
138
|
+
const turnNumber = ++nextTurnNumber;
|
|
139
|
+
scriptedHost?.beginTurn(turnNumber);
|
|
140
|
+
scenarioRuntime?.beginTurn(`turn-${turnNumber}`, signal);
|
|
141
|
+
let result;
|
|
91
142
|
try {
|
|
92
|
-
|
|
143
|
+
result = turnNumber === 1
|
|
144
|
+
? await session.start({ message })
|
|
145
|
+
: await session.reply({ message });
|
|
93
146
|
}
|
|
94
147
|
catch (error) {
|
|
95
|
-
|
|
96
|
-
|
|
148
|
+
try {
|
|
149
|
+
await scenarioRuntime?.failTurn(error);
|
|
150
|
+
}
|
|
151
|
+
catch (lifecycleError) {
|
|
152
|
+
if (lifecycleError instanceof TrialExecutionError) {
|
|
153
|
+
primaryTurnError = lifecycleError;
|
|
154
|
+
throw lifecycleError;
|
|
155
|
+
}
|
|
156
|
+
const combined = new AggregateError([error, lifecycleError], 'Provider turn failed and scenario settlement also failed');
|
|
157
|
+
primaryTurnError = combined;
|
|
158
|
+
throw combined;
|
|
159
|
+
}
|
|
160
|
+
primaryTurnError = error;
|
|
97
161
|
throw error;
|
|
98
162
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
163
|
+
if (result.exitCode !== 0 || result.timedOut) {
|
|
164
|
+
const providerTerminalError = new Error(`Provider terminal: exit ${result.exitCode}`);
|
|
165
|
+
primaryTurnError = providerTerminalError;
|
|
166
|
+
try {
|
|
167
|
+
await scenarioRuntime?.failTurn(providerTerminalError);
|
|
168
|
+
}
|
|
169
|
+
catch (lifecycleError) {
|
|
170
|
+
if (lifecycleError instanceof TrialExecutionError) {
|
|
171
|
+
primaryTurnError = lifecycleError;
|
|
172
|
+
throw lifecycleError;
|
|
173
|
+
}
|
|
174
|
+
const combined = new AggregateError([providerTerminalError, lifecycleError], 'Provider terminal failed and scenario settlement also failed');
|
|
175
|
+
primaryTurnError = combined;
|
|
176
|
+
throw combined;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
await scenarioRuntime?.settleTurn();
|
|
181
|
+
}
|
|
182
|
+
if (scriptedHost) {
|
|
183
|
+
const settled = scriptedHost.settleEvents(result.toolEvents);
|
|
184
|
+
result.toolEvents = settled.events;
|
|
185
|
+
if (settled.error) {
|
|
186
|
+
result = cloneTurnResultWithSensitiveValues(result, {
|
|
187
|
+
exitCode: 1,
|
|
188
|
+
rawOutput: settled.error.message,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const sensitiveValues = [...new Set([
|
|
193
|
+
...(deps.sensitiveValues ?? []),
|
|
194
|
+
...getTurnResultSensitiveValues(result),
|
|
195
|
+
])];
|
|
196
|
+
for (const event of result.toolEvents) {
|
|
197
|
+
attachToolEventSensitiveValues(event, [...new Set([
|
|
198
|
+
...sensitiveValues,
|
|
199
|
+
...getToolEventSensitiveValues(event),
|
|
200
|
+
])]);
|
|
113
201
|
}
|
|
202
|
+
return attachTurnResultSensitiveValues(result, sensitiveValues);
|
|
114
203
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
for (const event of result.toolEvents) {
|
|
120
|
-
attachToolEventSensitiveValues(event, [...new Set([
|
|
121
|
-
...sensitiveValues,
|
|
122
|
-
...getToolEventSensitiveValues(event),
|
|
123
|
-
])]);
|
|
204
|
+
finally {
|
|
205
|
+
finishTurnJoin();
|
|
206
|
+
if (inFlightTurn === turnJoin)
|
|
207
|
+
inFlightTurn = undefined;
|
|
124
208
|
}
|
|
125
|
-
return attachTurnResultSensitiveValues(result, sensitiveValues);
|
|
126
209
|
}, remaining, label);
|
|
127
210
|
}
|
|
128
211
|
finally {
|
|
@@ -166,12 +249,36 @@ export function createManagedSession(deps) {
|
|
|
166
249
|
if (disposePromise)
|
|
167
250
|
return disposePromise;
|
|
168
251
|
disposePromise = (async () => {
|
|
252
|
+
const cleanupErrors = [];
|
|
169
253
|
try {
|
|
170
254
|
await session?.dispose?.();
|
|
171
255
|
}
|
|
172
|
-
|
|
256
|
+
catch (error) {
|
|
257
|
+
cleanupErrors.push(error);
|
|
258
|
+
}
|
|
259
|
+
if (inFlightTurn) {
|
|
260
|
+
try {
|
|
261
|
+
await withAbortTimeout(async () => inFlightTurn, Math.min(agentTimeoutMs, 2_000), 'Agent turn cleanup');
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
cleanupErrors.push(error);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
173
268
|
await scriptedHost?.dispose();
|
|
174
269
|
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
cleanupErrors.push(error);
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
await scenarioRuntime?.dispose();
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
cleanupErrors.push(error);
|
|
278
|
+
}
|
|
279
|
+
if (cleanupErrors.length > 0) {
|
|
280
|
+
throw new AggregateError(primaryTurnError === undefined ? cleanupErrors : [primaryTurnError, ...cleanupErrors], primaryTurnError === undefined ? 'Agent session cleanup failed' : 'Provider turn failed and cleanup also failed');
|
|
281
|
+
}
|
|
175
282
|
})();
|
|
176
283
|
return disposePromise;
|
|
177
284
|
},
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { EvidenceEnvelope } from '../internal/direct-mcp-v2/types.js';
|
|
2
|
+
export type ExpectedScenarioCaseMatch = {
|
|
3
|
+
caseId?: string;
|
|
4
|
+
serverId?: string;
|
|
5
|
+
toolId?: string;
|
|
6
|
+
turnId?: string;
|
|
7
|
+
};
|
|
8
|
+
export type ScenarioCaseMatchEvidence = {
|
|
9
|
+
trialId: string;
|
|
10
|
+
turnId: string;
|
|
11
|
+
invocationId: string;
|
|
12
|
+
serverId: string;
|
|
13
|
+
toolId: string;
|
|
14
|
+
caseId: string;
|
|
15
|
+
stateBefore: string;
|
|
16
|
+
};
|
|
17
|
+
export type ScenarioStateTransitionEvidence = ScenarioCaseMatchEvidence & {
|
|
18
|
+
stateAfter: string;
|
|
19
|
+
committedEpoch: number;
|
|
20
|
+
};
|
|
21
|
+
export declare class ScenarioEvidenceError extends Error {
|
|
22
|
+
constructor(message: string);
|
|
23
|
+
}
|
|
24
|
+
export declare function findScenarioCaseMatches(evidence: readonly EvidenceEnvelope[], expected?: ExpectedScenarioCaseMatch): ScenarioCaseMatchEvidence[];
|
|
25
|
+
export declare function wasScenarioCaseMatched(evidence: readonly EvidenceEnvelope[], expected: ExpectedScenarioCaseMatch): boolean;
|
|
26
|
+
export declare function getScenarioStateTimeline(evidence: readonly EvidenceEnvelope[]): ScenarioStateTransitionEvidence[];
|
|
27
|
+
export declare function getFinalScenarioState(evidence: readonly EvidenceEnvelope[]): string;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { verifyEvidenceChain } from '../internal/direct-mcp-v2/evidence.js';
|
|
2
|
+
export class ScenarioEvidenceError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = 'ScenarioEvidenceError';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export function findScenarioCaseMatches(evidence, expected = {}) {
|
|
9
|
+
const invocations = canonicalInvocations(evidence);
|
|
10
|
+
const matches = [];
|
|
11
|
+
for (const envelope of evidence) {
|
|
12
|
+
const decision = envelope.payload;
|
|
13
|
+
if (decision.type !== 'mcp.decision' || decision.outcome !== 'matched' || !decision.caseId)
|
|
14
|
+
continue;
|
|
15
|
+
const invocation = invocations.get(decision.invocationId);
|
|
16
|
+
if (!invocation)
|
|
17
|
+
throw new ScenarioEvidenceError(`matched decision references unknown invocation ${decision.invocationId}`);
|
|
18
|
+
const match = { ...invocation, caseId: decision.caseId };
|
|
19
|
+
if (matchesExpectedCase(match, expected))
|
|
20
|
+
matches.push(match);
|
|
21
|
+
}
|
|
22
|
+
return matches;
|
|
23
|
+
}
|
|
24
|
+
export function wasScenarioCaseMatched(evidence, expected) {
|
|
25
|
+
return findScenarioCaseMatches(evidence, expected).length > 0;
|
|
26
|
+
}
|
|
27
|
+
export function getScenarioStateTimeline(evidence) {
|
|
28
|
+
const matches = new Map(findScenarioCaseMatches(evidence).map((entry) => [entry.invocationId, entry]));
|
|
29
|
+
const timeline = [];
|
|
30
|
+
for (const envelope of evidence) {
|
|
31
|
+
const commit = envelope.payload;
|
|
32
|
+
if (commit.type !== 'mcp.commit')
|
|
33
|
+
continue;
|
|
34
|
+
const match = matches.get(commit.invocationId);
|
|
35
|
+
if (!match)
|
|
36
|
+
throw new ScenarioEvidenceError(`commit references unmatched invocation ${commit.invocationId}`);
|
|
37
|
+
if (match.stateBefore !== commit.stateBefore) {
|
|
38
|
+
throw new ScenarioEvidenceError(`commit state disagrees with invocation ${commit.invocationId}`);
|
|
39
|
+
}
|
|
40
|
+
timeline.push({
|
|
41
|
+
...match,
|
|
42
|
+
stateAfter: commit.stateAfter,
|
|
43
|
+
committedEpoch: commit.committedEpoch,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return timeline;
|
|
47
|
+
}
|
|
48
|
+
export function getFinalScenarioState(evidence) {
|
|
49
|
+
assertCanonicalEvidence(evidence);
|
|
50
|
+
const payload = evidence.at(-1)?.payload;
|
|
51
|
+
if (payload?.type === 'turn.finalized' && payload.state !== undefined)
|
|
52
|
+
return payload.state;
|
|
53
|
+
throw new ScenarioEvidenceError('canonical evidence does not end with a finalized scenario state');
|
|
54
|
+
}
|
|
55
|
+
function canonicalInvocations(evidence) {
|
|
56
|
+
assertCanonicalEvidence(evidence);
|
|
57
|
+
const invocations = new Map();
|
|
58
|
+
for (const envelope of evidence) {
|
|
59
|
+
const invocation = envelope.payload;
|
|
60
|
+
if (invocation.type !== 'mcp.invocation')
|
|
61
|
+
continue;
|
|
62
|
+
if (invocations.has(invocation.invocationId)) {
|
|
63
|
+
throw new ScenarioEvidenceError(`duplicate invocation ${invocation.invocationId}`);
|
|
64
|
+
}
|
|
65
|
+
invocations.set(invocation.invocationId, {
|
|
66
|
+
trialId: envelope.trialId,
|
|
67
|
+
turnId: envelope.turnId,
|
|
68
|
+
invocationId: invocation.invocationId,
|
|
69
|
+
serverId: invocation.serverId,
|
|
70
|
+
toolId: invocation.toolId,
|
|
71
|
+
stateBefore: invocation.stateBefore,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return invocations;
|
|
75
|
+
}
|
|
76
|
+
function assertCanonicalEvidence(evidence) {
|
|
77
|
+
const first = evidence[0];
|
|
78
|
+
if (!first)
|
|
79
|
+
throw new ScenarioEvidenceError('canonical evidence is empty');
|
|
80
|
+
const chain = verifyEvidenceChain(evidence, first.artifactDigest);
|
|
81
|
+
if (!chain.ok)
|
|
82
|
+
throw new ScenarioEvidenceError(`canonical evidence chain failed at index ${chain.index}: ${chain.reason}`);
|
|
83
|
+
for (const [index, envelope] of evidence.entries()) {
|
|
84
|
+
if (envelope.trialId !== first.trialId) {
|
|
85
|
+
throw new ScenarioEvidenceError(`canonical evidence changes trialId at index ${index}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function matchesExpectedCase(match, expected) {
|
|
90
|
+
return (expected.caseId === undefined || match.caseId === expected.caseId)
|
|
91
|
+
&& (expected.serverId === undefined || match.serverId === expected.serverId)
|
|
92
|
+
&& (expected.toolId === undefined || match.toolId === expected.toolId)
|
|
93
|
+
&& (expected.turnId === undefined || match.turnId === expected.turnId);
|
|
94
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CompileScenarioResult } from '../internal/direct-mcp-v2/compiler.js';
|
|
2
|
+
import type { ScenarioMachineV2 as InternalScenarioMachineV2 } from '../internal/direct-mcp-v2/types.js';
|
|
3
|
+
/** Compile the public V2 source contract, whose concurrency safety policy is fixed by the version. */
|
|
4
|
+
export declare function compileScenario(source: unknown): CompileScenarioResult;
|
|
5
|
+
/** Compile strict JSON text using the public V2 source contract. */
|
|
6
|
+
export declare function compileScenarioText(text: string): CompileScenarioResult;
|
|
7
|
+
export type { CompileScenarioResult } from '../internal/direct-mcp-v2/compiler.js';
|
|
8
|
+
export type ScenarioMachineV2 = Pick<InternalScenarioMachineV2, 'version' | 'scenarioId' | 'initialState' | 'servers' | 'states'>;
|
|
9
|
+
export type { EvidenceEnvelope, JsonObject, JsonPrimitive, JsonValue, MatchExpr, MatchPredicate, McpContent, ScenarioArtifact, ScenarioCase, ScenarioDiagnostic, ScenarioEvidence, ScenarioRejection, ScenarioSuccess, ScenarioToolError, } from '../internal/direct-mcp-v2/types.js';
|