@parcha/agentrun-pi 0.1.0-beta.2 → 0.1.0-beta.3

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.
Files changed (52) hide show
  1. package/README.md +46 -19
  2. package/dist/cli.js +5 -3
  3. package/dist/demo.js +1 -0
  4. package/dist/extension-service.js +11 -4
  5. package/dist/extension.js +329 -57
  6. package/dist/host-addendum.d.ts +4 -0
  7. package/dist/host-addendum.js +19 -0
  8. package/dist/index.d.ts +1 -2
  9. package/dist/index.js +1 -2
  10. package/dist/presentation.js +2 -2
  11. package/dist/skills/author/SKILL.md +135 -0
  12. package/{skills → dist/skills}/author/references/jev-decisions.md +9 -8
  13. package/dist/skills/author/references/language.md +128 -0
  14. package/dist/skills/author/references/sdk-authoring.md +9 -0
  15. package/{skills → dist/skills}/author/references/workflow-format.md +7 -15
  16. package/dist/triage-demo.d.ts +62 -0
  17. package/dist/triage-demo.js +153 -0
  18. package/dist/types.d.ts +3 -0
  19. package/dist/workflow-inspector.d.ts +47 -0
  20. package/dist/workflow-inspector.js +235 -0
  21. package/dist/workflow-session.d.ts +19 -0
  22. package/dist/workflow-session.js +167 -0
  23. package/dist/workflow-store.d.ts +30 -0
  24. package/dist/workflow-store.js +249 -0
  25. package/dist/workflow-view.d.ts +64 -0
  26. package/dist/workflow-view.js +268 -0
  27. package/package.json +5 -6
  28. package/dist/admission.d.ts +0 -6
  29. package/dist/admission.js +0 -51
  30. package/dist/author.d.ts +0 -21
  31. package/dist/author.js +0 -72
  32. package/dist/benchmark-contract.d.ts +0 -88
  33. package/dist/benchmark-contract.js +0 -50
  34. package/dist/benchmark-freeze.d.ts +0 -9
  35. package/dist/benchmark-freeze.js +0 -9
  36. package/dist/benchmark.d.ts +0 -127
  37. package/dist/benchmark.js +0 -260
  38. package/dist/comparison.d.ts +0 -69
  39. package/dist/comparison.js +0 -229
  40. package/dist/skill-bundle.d.ts +0 -5
  41. package/dist/skill-bundle.js +0 -15
  42. package/skills/author/SKILL.md +0 -102
  43. package/skills/author/references/sdk-authoring.md +0 -16
  44. /package/{skills → dist/skills}/author/examples/evidence-gate.input.json +0 -0
  45. /package/{skills → dist/skills}/author/examples/evidence-gate.json +0 -0
  46. /package/{skills → dist/skills}/author/examples/read-file.input.json +0 -0
  47. /package/{skills → dist/skills}/author/examples/read-file.json +0 -0
  48. /package/{skills → dist/skills}/author/examples/read-source-decision.input.json +0 -0
  49. /package/{skills → dist/skills}/author/examples/read-source-decision.json +0 -0
  50. /package/{skills → dist/skills}/author/examples/release-note.txt +0 -0
  51. /package/{skills → dist/skills}/author/examples/review-release-notes.input.json +0 -0
  52. /package/{skills → dist/skills}/author/examples/review-release-notes.json +0 -0
package/dist/benchmark.js DELETED
@@ -1,260 +0,0 @@
1
- import { isDeepStrictEqual } from 'node:util';
2
- import { frozenBenchmarkContract } from './benchmark-freeze.js';
3
- import { Compile } from 'typebox/compile';
4
- import { formatWorkflowTree, runWorkflowSlice } from '@parcha/agentrun-dsl';
5
- import { createPiHostSessionFactory } from './host-session.js';
6
- import { createPiRunner, PiRunError } from './runner.js';
7
- import { WorkflowExtensionService, ExtensionServiceError } from './extension-service.js';
8
- import { benchmarkCases, benchmarkContract, benchmarkHash, benchmarkInstructions, benchmarkReportSchema, benchmarkEvidenceSchema, evaluatePiBenchmark } from './benchmark-contract.js';
9
- const emptyMetrics = () => ({ pi: 0, jev: 0, tools: 0, submissions: 0, tokens: 0, usageSamples: 0, knownCostUsd: 0, costSamples: 0 });
10
- const ceilings = { timeoutMs: 600_000, maxModelRequests: 24, maxToolCalls: 48, maxTurns: 24, maxSubmissions: 4 };
11
- class BenchmarkStop extends Error {
12
- code;
13
- constructor(code) {
14
- super(code);
15
- this.code = code;
16
- }
17
- }
18
- const textResult = (value) => ({ content: [{ type: 'text', text: JSON.stringify(value) }], details: value });
19
- const selectionWorkflow = { v: 2, name: 'benchmark-evidence', schemas: { Evidence: benchmarkEvidenceSchema, Result: { type: 'object' } }, output: { schemaId: 'Result', path: 'evidence' }, root: { node: 'sift', label: 'screen', itemsPath: 'sources', state: { question: '{question}' }, out: 'Evidence', as: 'evidence', keep: { path: 'relevant', gte: 0.8 } } };
20
- /** End-to-end comparison: DSL authoring, repair, execution and delivery all count.
21
- * Uses the selected host model, never a model selector or alternate provider. */
22
- export async function runPiSkillBenchmark(options) {
23
- if (options.mode !== 'live' && options.mode !== 'scripted-transport')
24
- throw new Error('Choose live or scripted-transport explicitly');
25
- if (typeof options.skillText !== 'string' || !options.skillText.trim() || Buffer.byteLength(options.skillText) > 128 * 1024)
26
- throw new Error('Provide the exact native skill bundle, at most 128 KiB');
27
- if (typeof options.runJudge !== 'function')
28
- throw new Error('Both arms require the same Jev capability');
29
- const suite = benchmarkCases(options.split);
30
- const contract = benchmarkContract();
31
- if (!isDeepStrictEqual(contract, frozenBenchmarkContract))
32
- throw new Error('Frozen benchmark contract changed; establish a new version and baseline');
33
- const skillText = options.skillText;
34
- const budgets = { ...ceilings, ...options.budgets };
35
- for (const key of Object.keys(ceilings))
36
- if (!Number.isSafeInteger(budgets[key]) || budgets[key] < 1 || budgets[key] > ceilings[key])
37
- throw new Error(`Invalid benchmark budget: ${key}`);
38
- const captured = { ...options.host };
39
- createPiHostSessionFactory(captured);
40
- const model = captured.model;
41
- const runs = [];
42
- for (const [index, item] of suite.entries()) {
43
- if (options.signal?.aborted)
44
- break;
45
- const order = index % 2 ? ['skill-dsl', 'plain-pi'] : ['plain-pi', 'skill-dsl'];
46
- const arms = [];
47
- for (const arm of order) {
48
- if (options.signal?.aborted)
49
- break;
50
- arms.push(await runArm(item, arm));
51
- }
52
- runs.push({ caseId: item.id, inputSha256: benchmarkHash({ question: item.question, topics: item.topics, sources: item.sources }), order: arms.map(row => row.arm), arms });
53
- }
54
- if (!isDeepStrictEqual(contract, benchmarkContract()))
55
- throw new Error('Benchmark verifier changed during execution');
56
- return {
57
- mode: options.mode, split: options.split, status: options.signal?.aborted ? 'cancelled' : 'complete',
58
- passed: runs.length === suite.length && runs.every(row => row.arms.length === 2 && row.arms.every(arm => arm.status === 'passed')),
59
- contract, skillSha256: benchmarkHash(skillText), budgets,
60
- runtime: { model: model.id, provider: model.provider, api: model.api, thinking: captured.thinkingLevel ?? 'low' }, runs,
61
- limitations: ['Three fictional cases per split are not a statistical or real-world benchmark.', 'Gold checks cover statuses and citation sets; summary entailment remains unscored.', 'All outer-agent authoring, repair, final delivery, child execution and failed attempts count; initial package/skill development does not.', 'Request caps count adapter invocations, not provider-internal HTTP retries. There is no token or spending cap.', 'Known cost uses returned provider/SDK accounting only; missing samples are unknown, including failed requests.', 'Host caches are not reset; execution order alternates. Heldout cases must not be used to tune the skill.', 'The DSL arm uses the native bridge and service with a constrained benchmark tool surface; this is not a test of interactive skill discovery.'],
62
- };
63
- async function runArm(item, arm) {
64
- const started = performance.now();
65
- const notify = (stage, event, status) => {
66
- try {
67
- void Promise.resolve(options.onProgress?.({ caseId: item.id, arm, stage, event, ...(status ? { status } : {}) })).catch(() => { });
68
- }
69
- catch { /* UI hooks cannot change the measurement. */ }
70
- };
71
- notify('arm', 'start');
72
- notify('outer', 'start');
73
- const controller = new AbortController();
74
- const signal = controller.signal;
75
- const stop = (code) => { if (!signal.aborted)
76
- controller.abort(new BenchmarkStop(code)); };
77
- const externalAbort = () => stop('cancelled');
78
- options.signal?.addEventListener('abort', externalAbort, { once: true });
79
- if (options.signal?.aborted)
80
- externalAbort();
81
- const timer = setTimeout(() => stop('deadline'), budgets.timeoutMs);
82
- const interrupted = new Promise((_resolve, reject) => {
83
- signal.addEventListener('abort', () => reject(signal.reason), { once: true });
84
- if (signal.aborted)
85
- reject(signal.reason);
86
- });
87
- void interrupted.catch(() => { });
88
- const bounded = (promise) => Promise.race([promise, interrupted]);
89
- let stage = 'outer';
90
- const stages = { outer: emptyMetrics(), workflow: emptyMetrics() };
91
- const candidates = [], workflowRuns = [];
92
- const total = () => Object.fromEntries(Object.keys(stages.outer).map(key => [key, stages.outer[key] + stages.workflow[key]]));
93
- const admit = (kind) => {
94
- signal.throwIfAborted();
95
- const counts = total();
96
- if (kind === 'tools' ? counts.tools >= budgets.maxToolCalls : counts.pi + counts.jev >= budgets.maxModelRequests)
97
- stop(kind === 'tools' ? 'tool_limit' : 'model_limit');
98
- signal.throwIfAborted();
99
- stages[stage][kind]++;
100
- };
101
- const usage = (bucket, tokens, cost) => {
102
- if (typeof tokens === 'number' && Number.isFinite(tokens) && tokens >= 0) {
103
- bucket.tokens += tokens;
104
- bucket.usageSamples++;
105
- }
106
- if (typeof cost === 'number' && Number.isFinite(cost) && cost >= 0) {
107
- bucket.knownCostUsd += cost;
108
- bucket.costSamples++;
109
- }
110
- };
111
- const host = { ...captured, modelRegistry: {
112
- getAll: () => captured.modelRegistry.getAll(),
113
- streamSimple(selected, transcript, opts) {
114
- admit('pi');
115
- const bucket = stages[stage];
116
- const stream = captured.modelRegistry.streamSimple(selected, transcript, opts);
117
- void stream.result().then(result => {
118
- const value = result;
119
- usage(bucket, value.usage?.totalTokens, value.usage?.cost?.total);
120
- }).catch(() => { });
121
- return stream;
122
- },
123
- } };
124
- const runJudge = async (request) => {
125
- request.signal?.throwIfAborted();
126
- admit('jev');
127
- const bucket = stages[stage];
128
- const result = await bounded(options.runJudge({ ...request, signal: request.signal ? AbortSignal.any([signal, request.signal]) : signal }));
129
- usage(bucket, result.usage ? result.usage.input_tokens + result.usage.output_tokens : undefined, result.cost_usd);
130
- return result;
131
- };
132
- const domainTools = [
133
- { name: 'read_sources', label: 'Read sources', description: 'Return {sources:[{id,text}]} for this fictional task. Direct workflow calls return this same structured value. This is not web search.', parameters: { type: 'object', properties: {}, additionalProperties: false }, async execute() { return textResult({ sources: structuredClone(item.sources) }); } },
134
- { name: 'screen_evidence', label: 'Screen evidence', description: 'Jev System One decisions select concrete evidence including limitations and contradictions at threshold 0.8.', parameters: { type: 'object', additionalProperties: false, required: ['topicId', 'sourceIds'], properties: { topicId: { type: 'string' }, sourceIds: { type: 'array', uniqueItems: true, items: { type: 'string' } } } },
135
- async execute(_id, value, toolSignal) {
136
- const args = value;
137
- const topic = item.topics.find(row => row.id === args.topicId);
138
- if (!topic || args.sourceIds.some(id => !item.sources.some(source => source.id === id)))
139
- throw new Error('Unknown topic or source');
140
- const result = await bounded(runWorkflowSlice(selectionWorkflow, { question: topic.question, sources: item.sources.filter(source => args.sourceIds.includes(source.id)) }, { from: 'screen' }, { runJudge: request => runJudge({ ...request, signal: toolSignal }) }));
141
- if (result.status !== 'complete')
142
- throw new Error('Screening did not complete');
143
- return textResult(result.state.evidence);
144
- } },
145
- ];
146
- const service = new WorkflowExtensionService({ allowedTools: domainTools.map(tool => tool.name), limits: { deadlineMs: budgets.timeoutMs } });
147
- const input = { question: item.question, topics: structuredClone(item.topics) };
148
- let lastOutput;
149
- let workflowInput = input;
150
- try {
151
- const factory = createPiHostSessionFactory(host);
152
- const makeRunner = (tools) => createPiRunner({ model, thinkingLevel: captured.thinkingLevel, cwd: captured.cwd, signal, tools,
153
- maxTurns: budgets.maxTurns, maxSubmissions: budgets.maxSubmissions, timeoutMs: budgets.timeoutMs,
154
- sessionFactory: opts => factory({ ...opts, customTools: opts.customTools.map(tool => ({ ...tool, async execute(...args) {
155
- args[2]?.throwIfAborted();
156
- admit('tools');
157
- if (tool.name === 'submit')
158
- stages[stage].submissions++;
159
- const validator = Compile(tool.parameters);
160
- if (!validator.Check(args[1]))
161
- throw new Error('Tool input does not match its schema');
162
- return tool.execute(...args);
163
- } })) }),
164
- });
165
- const child = makeRunner(domainTools);
166
- const agentrun = { name: 'agentrun', label: 'AgentRun workflow', description: 'Describe available tools, inspect a workflow, then run the inspected workflow. Never run an inline candidate. No shell, code, file access or model selection.', parameters: { type: 'object', additionalProperties: false, required: ['action'], properties: { action: { enum: ['describe', 'inspect', 'run'] }, workflow: {}, input: { type: 'object' } } }, async execute(_id, value) {
167
- const args = value;
168
- if (args.action === 'inspect' && args.input !== undefined)
169
- workflowInput = args.input;
170
- if (args.action === 'describe')
171
- return textResult({ tools: domainTools.map(tool => ({ name: tool.name, description: tool.description, parameters: tool.parameters })), skill: true, pi: true, jev: true, running: false, workflow: !!service.inspect().current });
172
- if (args.action === 'inspect') {
173
- try {
174
- if (args.workflow === undefined) {
175
- const current = service.inspect().current;
176
- if (!current)
177
- throw new Error('Inspect a workflow first');
178
- return textResult({ digest: current.digest, tree: formatWorkflowTree(current.inspection) });
179
- }
180
- const prepared = service.prepare(args.workflow);
181
- candidates.push({ digest: prepared.digest, accepted: true, workflow: prepared.workflow });
182
- return textResult({ digest: prepared.digest, tree: formatWorkflowTree(prepared.inspection) });
183
- }
184
- catch (error) {
185
- candidates.push({ accepted: false, workflow: structuredClone(args.workflow) });
186
- throw error;
187
- }
188
- }
189
- if (args.action !== 'run' || args.workflow !== undefined)
190
- throw new Error('Inspect the workflow first; run without a workflow argument');
191
- notify('outer', 'end');
192
- stage = 'workflow';
193
- notify('workflow', 'start');
194
- try {
195
- const report = await bounded(service.run(args.input ?? workflowInput, { signal, deps: {
196
- // A candidate cannot change the captured host thinking setting.
197
- runNode: request => child({ ...request, thinking: undefined }), runJudge,
198
- runEffect: async (request) => {
199
- request.signal.throwIfAborted();
200
- admit('tools');
201
- const tool = domainTools.find(tool => tool.name === request.node.tool);
202
- if (!tool || !Compile(tool.parameters).Check(request.input))
203
- throw new Error('Unsupported tool input');
204
- return (await bounded(tool.execute('benchmark-effect', request.input, request.signal, undefined, host))).details;
205
- },
206
- } }));
207
- workflowRuns.push({ status: report.status, digest: report.digest, ...(report.output === undefined ? {} : { output: report.output }) });
208
- lastOutput = report.status === 'complete' ? report.output : undefined;
209
- return textResult(report);
210
- }
211
- finally {
212
- notify('workflow', 'end');
213
- stage = 'outer';
214
- notify('outer', 'start');
215
- }
216
- } };
217
- const outer = makeRunner(arm === 'skill-dsl' ? [...domainTools, agentrun] : domainTools);
218
- const output = await bounded(outer({ kind: 'agent', label: 'benchmark-outer',
219
- system: [benchmarkInstructions, ...(arm === 'skill-dsl' ? [skillText, 'Use agentrun to inspect and execute your workflow. Deliver its completed output unchanged with submit. The benchmark forbids code, shell, files and changing models. Available task tools and their contracts are returned by agentrun describe.'] : [])],
220
- user: JSON.stringify(input), schema: benchmarkReportSchema }));
221
- signal.throwIfAborted();
222
- if (arm === 'skill-dsl' && (lastOutput === undefined || !isDeepStrictEqual(lastOutput, output)))
223
- return finish({ status: 'rejected', output, failure: 'workflow_delivery_missing_or_changed' });
224
- const evaluation = evaluatePiBenchmark(item.id, output);
225
- return finish({ status: evaluation.passed ? 'passed' : 'rejected', output, evaluation });
226
- }
227
- catch (error) {
228
- return finish({ status: options.signal?.aborted ? 'cancelled' : 'failed', failure: signal.reason instanceof BenchmarkStop ? signal.reason.code : error instanceof PiRunError ? `pi_${error.reason}` : error instanceof ExtensionServiceError ? error.code : 'execution_failed' });
229
- }
230
- finally {
231
- clearTimeout(timer);
232
- options.signal?.removeEventListener('abort', externalAbort);
233
- stop('arm_finished');
234
- void service.dispose();
235
- }
236
- function finish(result) {
237
- const counts = total();
238
- notify('outer', 'end', result.status);
239
- notify('arm', 'end', result.status);
240
- return structuredClone({ arm, ...result, elapsedMs: Math.round(performance.now() - started), total: counts, stages, candidates, workflowRuns, costComplete: counts.costSamples === counts.pi + counts.jev, usageComplete: counts.usageSamples === counts.pi + counts.jev });
241
- }
242
- }
243
- }
244
- /** A development comparison receipt, not automatic promotion. Heldout is a final
245
- * confirmation set, never the optimization target for this helper. */
246
- export function comparePiSkillBenchmarks(baseline, candidate) {
247
- if (baseline.split !== 'development' || candidate.split !== 'development')
248
- throw new Error('Hillclimb comparisons use development only');
249
- if (baseline.status !== 'complete' || candidate.status !== 'complete' || baseline.mode !== candidate.mode || !isDeepStrictEqual(baseline.contract, candidate.contract) || !isDeepStrictEqual(baseline.budgets, candidate.budgets) || !isDeepStrictEqual(baseline.runtime, candidate.runtime))
250
- throw new Error('Incomparable benchmark receipts');
251
- const select = (report) => report.runs.map(run => ({ id: run.caseId, input: run.inputSha256, arm: run.arms.find(arm => arm.arm === 'skill-dsl') }));
252
- const before = select(baseline), after = select(candidate);
253
- if (before.length !== 3 || after.length !== 3 || !isDeepStrictEqual(before.map(row => [row.id, row.input]), after.map(row => [row.id, row.input])) || [...before, ...after].some(row => !row.arm))
254
- throw new Error('Incomplete benchmark receipts');
255
- const regressions = before.filter((row, i) => row.arm.status === 'passed' && after[i].arm.status !== 'passed').map(row => row.id);
256
- const sums = (rows) => ({ passed: rows.filter(row => row.arm.status === 'passed').length, requests: rows.reduce((sum, row) => sum + row.arm.total.pi + row.arm.total.jev, 0), tokens: rows.reduce((sum, row) => sum + row.arm.total.tokens, 0), usageComplete: rows.every(row => row.arm.usageComplete), knownCostUsd: rows.reduce((sum, row) => sum + row.arm.total.knownCostUsd, 0), costComplete: rows.every(row => row.arm.costComplete) });
257
- const previous = sums(before), current = sums(after);
258
- const improved = !regressions.length && (current.passed > previous.passed || current.passed === previous.passed && current.passed === 3 && previous.usageComplete && current.usageComplete && current.tokens < previous.tokens);
259
- return { mode: candidate.mode, baselineSkillSha256: baseline.skillSha256, candidateSkillSha256: candidate.skillSha256, regressions, previous, current, improved, eligibleForLiveHeldout: candidate.mode === 'live' && improved && current.passed === 3 };
260
- }
@@ -1,69 +0,0 @@
1
- import { type WorkflowDeps } from '@parcha/agentrun-dsl';
2
- import type { PiHostContext } from './types.js';
3
- export declare function evaluatePiComparison(caseId: string, output: unknown): {
4
- passed: boolean;
5
- issues: string[];
6
- };
7
- export interface PiComparisonBudgets {
8
- timeoutMs: number;
9
- maxModelRequests: number;
10
- maxToolCalls: number;
11
- maxTurns: number;
12
- maxSubmissions: number;
13
- }
14
- export interface PiComparisonOptions {
15
- host: PiHostContext;
16
- runJudge: NonNullable<WorkflowDeps['runJudge']>;
17
- signal?: AbortSignal;
18
- mode: 'live' | 'scripted-transport';
19
- budgets?: Partial<PiComparisonBudgets>;
20
- }
21
- export interface PiComparisonArm {
22
- arm: 'plain-pi' | 'workflow';
23
- status: 'passed' | 'rejected' | 'failed' | 'cancelled';
24
- elapsedMs: number;
25
- requests: {
26
- pi: number;
27
- jev: number;
28
- total: number;
29
- };
30
- toolCalls: number;
31
- submissions: number;
32
- tokens: {
33
- pi: number;
34
- jev: number;
35
- piUsageSamples: number;
36
- jevUsageSamples: number;
37
- };
38
- output?: unknown;
39
- evaluation?: ReturnType<typeof evaluatePiComparison>;
40
- failure?: string;
41
- }
42
- export declare function runPiComparison(options: PiComparisonOptions): Promise<{
43
- mode: "live" | "scripted-transport";
44
- status: "complete" | "cancelled";
45
- passed: boolean;
46
- runtime: {
47
- provider: string;
48
- model: string;
49
- api: string;
50
- thinking: import("./types.js").PiThinkingLevel;
51
- };
52
- budgets: {
53
- timeoutMs: number;
54
- maxModelRequests: number;
55
- maxToolCalls: number;
56
- maxTurns: number;
57
- maxSubmissions: number;
58
- };
59
- workflowSha256: string;
60
- suiteSha256: string;
61
- runs: {
62
- caseId: string;
63
- inputSha256: string;
64
- expectedSha256: string;
65
- executionOrder: string[];
66
- arms: PiComparisonArm[];
67
- }[];
68
- limits: string[];
69
- }>;
@@ -1,229 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import { Compile } from 'typebox/compile';
3
- import { runWorkflow, runWorkflowSlice, workflowSha256 } from '@parcha/agentrun-dsl';
4
- import { createPiHostSessionFactory } from './host-session.js';
5
- import { createPiRunner, PiRunError } from './runner.js';
6
- import { demoData } from './demo-data.js';
7
- const cases = [
8
- { id: 'single-fact', question: 'Can our contributors edit documentation in a browser?',
9
- topics: [{ id: 'editing', question: 'Can contributors edit documentation in a browser?' }],
10
- sources: [{ id: 'browser-trial', text: 'In our trial, all three contributors successfully edited a documentation page in the browser.' }, { id: 'advert', text: 'A beautiful new logo for documentation.' }],
11
- expected: [{ topicId: 'editing', status: 'supported', sourceIds: ['browser-trial'] }] },
12
- { id: 'parallel-research', question: 'What would change if our team moved its wiki into the code repository?',
13
- topics: [{ id: 'reviews', question: demoData.subquestions[0] }, { id: 'editing', question: demoData.subquestions[1] }, { id: 'search', question: demoData.subquestions[2] }],
14
- sources: structuredClone(demoData.sources),
15
- expected: [{ topicId: 'reviews', status: 'supported', sourceIds: ['review-guide'] }, { topicId: 'editing', status: 'supported', sourceIds: ['editor-trial'] }, { topicId: 'search', status: 'supported', sourceIds: ['search-trial'] }] },
16
- { id: 'gaps-and-conflicts', question: 'Does our repository trial support restricted editing and synonym search?',
17
- topics: [{ id: 'permissions', question: 'Who can edit restricted documentation pages in our trial?' }, { id: 'search', question: 'Did synonym search work in our trial?' }],
18
- sources: [{ id: 'permissions-heading', text: 'Restricted documentation permissions: details to be supplied.' }, { id: 'search-positive', text: 'The trial report says all ten synonym searches found the intended pages.' }, { id: 'search-negative', text: 'The audit of the same trial says none of those ten synonym searches found the intended pages.' }],
19
- expected: [{ topicId: 'permissions', status: 'insufficient', sourceIds: [] }, { topicId: 'search', status: 'conflicting', sourceIds: ['search-positive', 'search-negative'] }] },
20
- ];
21
- const str = { type: 'string', minLength: 1 };
22
- const findingSchema = { type: 'object', additionalProperties: false, required: ['topicId', 'status', 'summary', 'sourceIds'], properties: {
23
- topicId: str, status: { enum: ['supported', 'conflicting', 'insufficient'] }, summary: str, sourceIds: { type: 'array', uniqueItems: true, items: str },
24
- } };
25
- const reportSchema = { type: 'object', additionalProperties: false, required: ['answer', 'findings'], properties: {
26
- answer: str, findings: { type: 'array', minItems: 1, items: findingSchema },
27
- } };
28
- const sourcesSchema = { type: 'object', required: ['sources'], additionalProperties: false, properties: { sources: { type: 'array', items: { type: 'object', required: ['id', 'text'], additionalProperties: false, properties: { id: str, text: str } } } } };
29
- const evidenceSchema = demoData.workflow.root.steps[1].body.workflow.schemas.EvidenceDecision;
30
- const instructions = 'Use only the supplied fictional corpus. For each topic preserve all material sources, including contrary evidence, conditions and uncertainty. Status supported means the sources establish a finding, not necessarily a positive answer; conflicting means incompatible accounts; insufficient means no concrete information answers the topic. Cite source IDs. Do not turn qualified evidence into unconditional claims. You may read_sources or screen_evidence with Jev System One decisions. Submit the requested structured output.';
31
- const screen = { node: 'sift', label: 'screen-evidence', itemsPath: 'search.sources', state: { subquestion: '{item.question}' }, out: 'EvidenceDecision', as: 'evidence', keep: { path: 'answersQuestion', gte: 0.8 } };
32
- const workflow = {
33
- v: 2, name: 'comparison-research', schemas: { Input: { type: 'object' }, Sources: sourcesSchema, Finding: findingSchema, Report: reportSchema, EvidenceDecision: evidenceSchema },
34
- input: { schemaId: 'Input' }, output: { schemaId: 'Report', path: 'report' },
35
- root: { node: 'chain', steps: [
36
- { node: 'map', label: 'research-topics', itemsPath: 'topics', maxConcurrency: 3, resultPath: 'finding', as: 'findings', body: { node: 'chain', steps: [
37
- { node: 'call', label: 'read-sources', via: 'tool', tool: 'read_sources', args: {}, out: 'Sources', as: 'search', deadline_s: 15 }, screen,
38
- { node: 'agent', label: 'finding', instructions, state: { topic: '{item}', sources: '{evidence.items}' }, out: 'Finding', as: 'finding' },
39
- ] } },
40
- { node: 'agent', label: 'report', instructions, state: { question: '{question}', topics: '{topics}', findings: '{findings}' }, out: 'Report', as: 'report' },
41
- ] },
42
- };
43
- const selector = { ...workflow, name: 'comparison-screen', root: { node: 'chain', steps: [screen] } };
44
- const hash = (value) => createHash('sha256').update(JSON.stringify(value)).digest('hex');
45
- const validator = Compile(reportSchema);
46
- export function evaluatePiComparison(caseId, output) {
47
- const item = cases.find(row => row.id === caseId);
48
- if (!item)
49
- throw new Error('Unknown comparison case');
50
- if (!validator.Check(output))
51
- return { passed: false, issues: ['output_schema'] };
52
- const findings = output.findings;
53
- const issues = [];
54
- if (findings.length !== item.expected.length || new Set(findings.map(row => row.topicId)).size !== findings.length)
55
- issues.push('topic_coverage');
56
- for (const expected of item.expected) {
57
- const actual = findings.find(row => row.topicId === expected.topicId);
58
- if (!actual || actual.status !== expected.status)
59
- issues.push(`${expected.topicId}:status`);
60
- if (!actual || JSON.stringify([...actual.sourceIds].sort()) !== JSON.stringify([...expected.sourceIds].sort()))
61
- issues.push(`${expected.topicId}:citations`);
62
- }
63
- return { passed: issues.length === 0, issues };
64
- }
65
- class ComparisonStop extends Error {
66
- code;
67
- constructor(code) {
68
- super(code);
69
- this.code = code;
70
- }
71
- }
72
- const defaults = { timeoutMs: 120_000, maxModelRequests: 12, maxToolCalls: 24, maxTurns: 12, maxSubmissions: 3 };
73
- export async function runPiComparison(options) {
74
- if (options.mode !== 'live' && options.mode !== 'scripted-transport')
75
- throw new Error('Comparison requires an explicit execution mode');
76
- if (typeof options.runJudge !== 'function')
77
- throw new Error('Both arms require the same Jev capability');
78
- const budgets = { ...defaults, ...options.budgets };
79
- for (const key of Object.keys(defaults)) {
80
- if (!Number.isSafeInteger(budgets[key]) || budgets[key] < 1 || budgets[key] > defaults[key])
81
- throw new Error(`Comparison budget ${key} must be between 1 and ${defaults[key]}`);
82
- }
83
- const originalRegistry = options.host.modelRegistry;
84
- const capturedHost = { ...options.host, modelRegistry: originalRegistry };
85
- createPiHostSessionFactory(capturedHost);
86
- const model = capturedHost.model;
87
- const runs = [];
88
- for (const [index, item] of cases.entries()) {
89
- if (options.signal?.aborted)
90
- break;
91
- const order = index % 2 ? ['workflow', 'plain-pi'] : ['plain-pi', 'workflow'];
92
- const arms = [];
93
- for (const arm of order) {
94
- if (options.signal?.aborted)
95
- break;
96
- arms.push(await runArm(item, arm));
97
- }
98
- runs.push({ caseId: item.id, inputSha256: hash({ question: item.question, topics: item.topics, sources: item.sources }), expectedSha256: hash(item.expected), executionOrder: arms.map(arm => arm.arm), arms });
99
- }
100
- return {
101
- mode: options.mode, status: options.signal?.aborted ? 'cancelled' : 'complete',
102
- passed: runs.length === cases.length && runs.every(run => run.arms.length === 2 && run.arms.every(arm => arm.status === 'passed')),
103
- runtime: { provider: model.provider, model: model.id, api: model.api, thinking: capturedHost.thinkingLevel ?? 'low' },
104
- budgets, workflowSha256: workflowSha256(workflow), suiteSha256: hash(cases), runs,
105
- limits: ['Three fictional cases, one run per arm; not a statistical benchmark.', 'Workflow authoring and setup costs excluded; both arms receive identical prewritten topics.', 'Both arms can use the same corpus reader and Jev rubric; the plain loop chooses whether to call them.', 'Evaluation checks topic status and citation sets, not arbitrary prose entailment.', 'Token totals cover reported usage only; compare usage sample counts with request counts.', 'Host caches are not reset; execution order alternates.'],
106
- };
107
- async function runArm(item, arm) {
108
- const started = performance.now();
109
- const controller = new AbortController();
110
- const signal = controller.signal;
111
- const abort = (code) => { if (!signal.aborted)
112
- controller.abort(new ComparisonStop(code)); };
113
- const externalAbort = () => abort('cancelled');
114
- options.signal?.addEventListener('abort', externalAbort, { once: true });
115
- if (options.signal?.aborted)
116
- externalAbort();
117
- const timer = setTimeout(() => abort('deadline'), budgets.timeoutMs);
118
- let rejectAbort;
119
- const interrupted = new Promise((_resolve, reject) => { rejectAbort = reject; });
120
- const onAbort = () => rejectAbort(signal.reason);
121
- signal.addEventListener('abort', onAbort, { once: true });
122
- if (signal.aborted)
123
- onAbort();
124
- void interrupted.catch(() => { });
125
- const bounded = (work) => Promise.race([work, interrupted]);
126
- const requests = { pi: 0, jev: 0, total: 0 };
127
- const tokens = { pi: 0, jev: 0, piUsageSamples: 0, jevUsageSamples: 0 };
128
- let toolCalls = 0, submissions = 0;
129
- const admit = (kind) => {
130
- signal.throwIfAborted();
131
- if (kind === 'tool') {
132
- if (toolCalls >= budgets.maxToolCalls)
133
- abort('tool_call_limit');
134
- signal.throwIfAborted();
135
- toolCalls++;
136
- }
137
- else {
138
- if (requests.total >= budgets.maxModelRequests)
139
- abort('model_request_limit');
140
- signal.throwIfAborted();
141
- requests.total++;
142
- requests[kind]++;
143
- }
144
- };
145
- const host = { ...capturedHost, modelRegistry: {
146
- getAll: () => originalRegistry.getAll(),
147
- streamSimple(selected, transcript, streamOptions) {
148
- admit('pi');
149
- const stream = originalRegistry.streamSimple(selected, transcript, streamOptions);
150
- void stream.result().then(result => {
151
- const usage = result?.usage;
152
- if (Number.isFinite(usage?.totalTokens) && usage.totalTokens >= 0) {
153
- tokens.pi += usage.totalTokens;
154
- tokens.piUsageSamples++;
155
- }
156
- }).catch(() => { });
157
- return stream;
158
- },
159
- } };
160
- const runJudge = async (request) => {
161
- request.signal?.throwIfAborted();
162
- admit('jev');
163
- const result = await bounded(options.runJudge({ ...request, signal: request.signal ? AbortSignal.any([signal, request.signal]) : signal }));
164
- if (result.usage && Number.isFinite(result.usage.input_tokens) && Number.isFinite(result.usage.output_tokens) && result.usage.input_tokens >= 0 && result.usage.output_tokens >= 0) {
165
- tokens.jev += result.usage.input_tokens + result.usage.output_tokens;
166
- tokens.jevUsageSamples++;
167
- }
168
- return result;
169
- };
170
- const read = () => ({ sources: structuredClone(item.sources) });
171
- const tools = [
172
- { name: 'read_sources', label: 'Read sources', description: 'Read the complete fictional corpus for this task.', parameters: { type: 'object', properties: {}, additionalProperties: false },
173
- async execute() { const value = read(); return { content: [{ type: 'text', text: JSON.stringify(value) }], details: value }; } },
174
- { name: 'screen_evidence', label: 'Screen evidence', description: 'Use Jev System One decisions to retain concrete evidence for a topic, including qualified and contrary evidence. Same rubric and 0.8 threshold as the workflow.',
175
- parameters: { type: 'object', properties: { topicId: str, sourceIds: { type: 'array', uniqueItems: true, items: str } }, required: ['topicId', 'sourceIds'], additionalProperties: false },
176
- async execute(_id, value, toolSignal) {
177
- const args = value;
178
- const topic = item.topics.find(row => row.id === args.topicId);
179
- if (!topic || args.sourceIds.some(id => !item.sources.some(source => source.id === id)))
180
- throw new Error('Unknown topic or source');
181
- const selected = item.sources.filter(source => args.sourceIds.includes(source.id));
182
- const result = await bounded(runWorkflowSlice(selector, {}, { from: 'screen-evidence', seed: { item: topic, search: { sources: selected } } }, { runJudge: request => runJudge({ ...request, signal: toolSignal }) }));
183
- if (result.status !== 'complete')
184
- throw new Error('Evidence screening did not complete');
185
- const evidence = result.state.evidence;
186
- return { content: [{ type: 'text', text: JSON.stringify(evidence) }], details: evidence };
187
- } },
188
- ];
189
- try {
190
- const factory = createPiHostSessionFactory(host);
191
- const runNode = createPiRunner({ model, thinkingLevel: capturedHost.thinkingLevel, cwd: capturedHost.cwd, signal,
192
- maxTurns: budgets.maxTurns, maxSubmissions: budgets.maxSubmissions, timeoutMs: budgets.timeoutMs, tools,
193
- sessionFactory: opts => factory({ ...opts, customTools: opts.customTools.map(tool => ({ ...tool,
194
- async execute(...args) { args[2]?.throwIfAborted(); admit('tool'); if (tool.name === 'submit')
195
- submissions++; return tool.execute(...args); },
196
- })) }),
197
- });
198
- const input = { question: item.question, topics: structuredClone(item.topics) };
199
- const output = await bounded((async () => {
200
- if (arm === 'plain-pi')
201
- return runNode({ kind: 'agent', label: 'plain-pi', system: [instructions], user: JSON.stringify(input), schema: reportSchema });
202
- const result = await runWorkflow(workflow, input, { runNode, runJudge, runEffect: async ({ node }) => {
203
- if (node.tool !== 'read_sources')
204
- throw new Error('Unknown comparison tool');
205
- admit('tool');
206
- return read();
207
- } });
208
- if (result.status !== 'complete')
209
- throw new Error('Workflow did not complete');
210
- return result.output;
211
- })());
212
- signal.throwIfAborted();
213
- const evaluation = evaluatePiComparison(item.id, output);
214
- return finish({ status: evaluation.passed ? 'passed' : 'rejected', output, evaluation });
215
- }
216
- catch (error) {
217
- return finish({ status: options.signal?.aborted ? 'cancelled' : 'failed', failure: signal.reason instanceof ComparisonStop ? signal.reason.code : error instanceof PiRunError ? `pi_${error.reason}` : 'execution_failed' });
218
- }
219
- finally {
220
- clearTimeout(timer);
221
- options.signal?.removeEventListener('abort', externalAbort);
222
- signal.removeEventListener('abort', onAbort);
223
- abort('arm_finished');
224
- }
225
- function finish(result) {
226
- return { arm, ...result, elapsedMs: Math.round(performance.now() - started), requests: { ...requests }, toolCalls, submissions, tokens: { ...tokens } };
227
- }
228
- }
229
- }
@@ -1,5 +0,0 @@
1
- /** Shipped with the extension, including hosts without filesystem-reading tools. */
2
- export declare function loadPiJevGuide(): string;
3
- /** The same core contract for skill-enabled and tool-only Pi hosts. */
4
- export declare function loadPiWorkflowGuide(): string;
5
- export declare function loadPiAuthorSkillBundle(): string;
@@ -1,15 +0,0 @@
1
- import { readFileSync, readdirSync } from 'node:fs';
2
- /** Shipped with the extension, including hosts without filesystem-reading tools. */
3
- export function loadPiJevGuide() {
4
- return readFileSync(new URL('../skills/author/references/jev-decisions.md', import.meta.url), 'utf8');
5
- }
6
- /** The same core contract for skill-enabled and tool-only Pi hosts. */
7
- export function loadPiWorkflowGuide() {
8
- return readFileSync(new URL('../skills/author/references/workflow-format.md', import.meta.url), 'utf8');
9
- }
10
- export function loadPiAuthorSkillBundle() {
11
- const root = new URL('../skills/author/', import.meta.url);
12
- const files = ['SKILL.md', 'references/workflow-format.md', 'references/jev-decisions.md', ...readdirSync(new URL('examples/', root))
13
- .filter(name => name.endsWith('.json')).sort().map(name => `examples/${name}`)];
14
- return files.map(path => `\n--- ${path} ---\n${readFileSync(new URL(path, root), 'utf8')}`).join('\n');
15
- }