orizu 0.5.30 → 0.5.31
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/dist/gepa-engine-dispatch.d.ts.map +1 -1
- package/dist/gepa-engine-dispatch.js +16 -1
- package/dist/help.js +2 -2
- package/dist/hosted-gepa-cli.d.ts.map +1 -1
- package/dist/hosted-gepa-cli.js +7 -1
- package/dist/hosted-harness-claude.d.ts.map +1 -1
- package/dist/hosted-harness-claude.js +4 -1
- package/dist/hosted-harness-opencode.d.ts +1 -0
- package/dist/hosted-harness-opencode.d.ts.map +1 -1
- package/dist/hosted-harness-opencode.js +23 -7
- package/dist/hosted-harness.d.ts +30 -1
- package/dist/hosted-harness.d.ts.map +1 -1
- package/dist/hosted-harness.js +5 -0
- package/dist/hosted-tool-hints.d.ts +56 -0
- package/dist/hosted-tool-hints.d.ts.map +1 -0
- package/dist/hosted-tool-hints.js +105 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -5
- package/dist/instruction-set-gepa-launch.d.ts +21 -2
- package/dist/instruction-set-gepa-launch.d.ts.map +1 -1
- package/dist/instruction-set-gepa-launch.js +123 -3
- package/dist/optimizations-promote-cli.d.ts.map +1 -1
- package/dist/optimizations-promote-cli.js +19 -0
- package/dist/seed-against-production.d.ts +11 -0
- package/dist/seed-against-production.d.ts.map +1 -0
- package/dist/seed-against-production.js +1 -0
- package/package.json +1 -1
- package/src/gepa-engine-dispatch.ts +18 -1
- package/src/help.ts +2 -2
- package/src/hosted-gepa-cli.ts +7 -1
- package/src/hosted-harness-claude.ts +4 -1
- package/src/hosted-harness-opencode.ts +24 -7
- package/src/hosted-harness.ts +32 -1
- package/src/hosted-tool-hints.ts +153 -0
- package/src/index.ts +2 -5
- package/src/instruction-set-gepa-launch.ts +161 -3
- package/src/optimizations-promote-cli.ts +20 -2
- package/src/seed-against-production.ts +10 -0
- package/vendor/skills/orizu/references/cli-reference.md +1 -1
- package/vendor/skills/orizu/references/migrate-existing-gepa-setup.md +1 -2
- package/vendor/skills/orizu/references/optimization-reports.md +1 -1
- package/vendor/skills/orizu/references/optimization-with-gepa.md +1 -1
- package/vendor/skills/orizu/references/prompt-control-plane.md +6 -1
|
@@ -1,17 +1,137 @@
|
|
|
1
1
|
import { authedFetch } from './http.js';
|
|
2
|
+
import { MAX_VERSION_NUMBER, parseSpecifier } from './instruction-set-lock/index.js';
|
|
2
3
|
import { parseJsonResponse } from './json-response.js';
|
|
3
|
-
/**
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* A version runs and promotes only when its component keys equal the set's
|
|
6
|
+
* current shape exactly: promotion refuses any other key set
|
|
7
|
+
* (instruction_set_candidate_shape_mismatch). Returns null when they match.
|
|
8
|
+
*/
|
|
9
|
+
function shapeDifference(shape, version) {
|
|
10
|
+
const keys = new Set(version.componentKeys ?? []);
|
|
11
|
+
const shapeKeys = new Set(shape);
|
|
12
|
+
const missing = shape.filter(key => !keys.has(key));
|
|
13
|
+
const extra = [...keys].filter(key => !shapeKeys.has(key)).sort();
|
|
14
|
+
return missing.length === 0 && extra.length === 0 ? null : { missing, extra };
|
|
15
|
+
}
|
|
16
|
+
function describeDifference({ missing, extra }) {
|
|
17
|
+
const quote = (keys) => keys.map(key => `"${key}"`).join(', ');
|
|
18
|
+
return [
|
|
19
|
+
...(missing.length > 0 ? [`missing ${quote(missing)}`] : []),
|
|
20
|
+
...(extra.length > 0 ? [`extra ${quote(extra)}`] : []),
|
|
21
|
+
].join('; ');
|
|
22
|
+
}
|
|
23
|
+
function newestMatchingVersion(shape, versions) {
|
|
24
|
+
return versions
|
|
25
|
+
.filter(candidate => Array.isArray(candidate.componentKeys) && shapeDifference(shape, candidate) === null)
|
|
26
|
+
.reduce((max, candidate) => {
|
|
27
|
+
const number = candidate.versionNumber ?? 0;
|
|
28
|
+
return max === null || number > max ? number : max;
|
|
29
|
+
}, null);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* ORI-2128: a version made before a shape change can lack a component of the
|
|
33
|
+
* set's current shape, or carry one the set has since removed. The runner or
|
|
34
|
+
* promotion would refuse it later; refuse it here, before a run is spawned or
|
|
35
|
+
* queued. A server that predates per-version component keys sends none; the
|
|
36
|
+
* runner remains the backstop.
|
|
37
|
+
*/
|
|
38
|
+
function requireMatchingSeed(shape, versions, version, modelConfigIdentity, role) {
|
|
39
|
+
if (!Array.isArray(shape) || !Array.isArray(version.componentKeys))
|
|
40
|
+
return;
|
|
41
|
+
const difference = shapeDifference(shape, version);
|
|
42
|
+
if (difference === null)
|
|
43
|
+
return;
|
|
44
|
+
const newest = newestMatchingVersion(shape, versions);
|
|
45
|
+
const detail = describeDifference(difference);
|
|
46
|
+
if (role === 'production') {
|
|
47
|
+
const newestNote = newest === null ? 'no version of this profile matches it yet' : `newest matching: v${newest}`;
|
|
48
|
+
throw new Error(`instruction_set_profile_production_incomplete: ${modelConfigIdentity} production v${version.versionNumber} does not match the current shape (${detail}); promote a version that matches it, or name one with @vN (${newestNote})`);
|
|
49
|
+
}
|
|
50
|
+
const advice = newest === null
|
|
51
|
+
? 'no version of this profile matches the current shape yet'
|
|
52
|
+
: `pick v${newest}`;
|
|
53
|
+
throw new Error(`instruction_set_profile_version_incomplete: ${modelConfigIdentity} v${version.versionNumber} does not match the current shape (${detail}); ${advice}`);
|
|
54
|
+
}
|
|
55
|
+
function profileVersionNumber(value) {
|
|
56
|
+
const match = /^v?([1-9][0-9]*)$/u.exec(value);
|
|
57
|
+
const versionNumber = match ? Number(match[1]) : Number.NaN;
|
|
58
|
+
if (!Number.isSafeInteger(versionNumber) || versionNumber > MAX_VERSION_NUMBER) {
|
|
59
|
+
throw new Error(`--profile-version must be a version number such as 3 or v3; received ${value}`);
|
|
60
|
+
}
|
|
61
|
+
return versionNumber;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Combine `--instruction-set`, `--model-config` and `--profile-version` into
|
|
65
|
+
* one Seed. A value with `/` or `@` is a specifier (`set/profile@vN`); any
|
|
66
|
+
* other value is a set slug or exact name, passed through unchanged. Values
|
|
67
|
+
* named twice must agree, the same rule `sync` applies to `--version`.
|
|
68
|
+
*/
|
|
69
|
+
export function gepaSeedSelector(instructionSetArg, modelConfigFlag, versionFlag) {
|
|
70
|
+
const flagVersion = versionFlag === undefined ? undefined : profileVersionNumber(versionFlag);
|
|
71
|
+
if (!instructionSetArg.includes('/') && !instructionSetArg.includes('@')) {
|
|
72
|
+
return { instructionSetName: instructionSetArg, modelConfigIdentity: modelConfigFlag, versionNumber: flagVersion };
|
|
73
|
+
}
|
|
74
|
+
const parsed = parseSpecifier(instructionSetArg);
|
|
75
|
+
if (parsed.profile !== undefined && modelConfigFlag !== undefined && parsed.profile !== modelConfigFlag) {
|
|
76
|
+
throw new Error(`instruction_set_seed_conflict: specifier names profile ${parsed.profile} but --model-config is ${modelConfigFlag}`);
|
|
77
|
+
}
|
|
78
|
+
if (parsed.versionNumber !== undefined && flagVersion !== undefined && parsed.versionNumber !== flagVersion) {
|
|
79
|
+
throw new Error(`instruction_set_seed_conflict: specifier names v${parsed.versionNumber} but --profile-version is v${flagVersion}`);
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
instructionSetName: parsed.set,
|
|
83
|
+
modelConfigIdentity: parsed.profile ?? modelConfigFlag,
|
|
84
|
+
versionNumber: parsed.versionNumber ?? flagVersion,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Resolve the Seed's profile version id: the named version when one is
|
|
89
|
+
* given, otherwise the requested Profile's Production.
|
|
90
|
+
*/
|
|
91
|
+
async function resolveGepaInstructionSetProfileVersion(instructionSetName, modelConfigIdentity, project, versionNumber) {
|
|
5
92
|
const response = await authedFetch(`/api/cli/instruction-sets/${encodeURIComponent(instructionSetName)}?project=${encodeURIComponent(project)}`);
|
|
6
93
|
if (!response.ok)
|
|
7
94
|
throw new Error(`Instruction set resolution failed: ${await response.text()}`);
|
|
8
95
|
const payload = await parseJsonResponse(response, 'Instruction set resolution');
|
|
9
96
|
const instructionSet = payload.instructionSet;
|
|
10
97
|
const profile = instructionSet?.profiles?.find(candidate => candidate.modelConfigIdentity === modelConfigIdentity);
|
|
11
|
-
|
|
98
|
+
// An archived model config's profile is not resolvable: the same refusal
|
|
99
|
+
// the server's resolve_instruction_set gives hosted runs.
|
|
100
|
+
if (!profile || profile.modelConfigArchivedAt)
|
|
12
101
|
throw new Error(`instruction_set_profile_not_found: ${modelConfigIdentity}`);
|
|
102
|
+
const versions = profile.versions ?? [];
|
|
103
|
+
if (versionNumber !== undefined) {
|
|
104
|
+
const version = versions.find(candidate => candidate.versionNumber === versionNumber);
|
|
105
|
+
if (version?.profileVersionId) {
|
|
106
|
+
requireMatchingSeed(instructionSet?.shape, versions, version, modelConfigIdentity, 'named');
|
|
107
|
+
return version.profileVersionId;
|
|
108
|
+
}
|
|
109
|
+
const newest = versions.reduce((max, candidate) => Math.max(max, candidate.versionNumber ?? 0), 0);
|
|
110
|
+
throw new Error(`instruction_set_profile_version_not_found: ${modelConfigIdentity} has no v${versionNumber}; ${newest > 0 ? `newest is v${newest}` : 'it has no versions'}`);
|
|
111
|
+
}
|
|
13
112
|
const profileVersionId = profile.production?.profileVersionId;
|
|
14
113
|
if (!profileVersionId)
|
|
15
114
|
throw new Error(`instruction_set_profile_not_promoted: ${modelConfigIdentity}`);
|
|
115
|
+
const production = versions.find(candidate => candidate.profileVersionId === profileVersionId);
|
|
116
|
+
if (production)
|
|
117
|
+
requireMatchingSeed(instructionSet?.shape, versions, production, modelConfigIdentity, 'production');
|
|
16
118
|
return profileVersionId;
|
|
17
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Resolve the Seed that dispatch normalised into the connector environment.
|
|
122
|
+
* Returns undefined when no instruction set was selected. When no version is
|
|
123
|
+
* named, `unversioned: 'production'` resolves the profile's Production here
|
|
124
|
+
* (local runs); `unversioned: 'server'` leaves it to the hosted launch route,
|
|
125
|
+
* which resolves Production itself in the same call that queues the run.
|
|
126
|
+
*/
|
|
127
|
+
export async function resolveGepaSeedFromEnvironment(environment, project, unversioned) {
|
|
128
|
+
const instructionSetName = environment.ORIZU_INSTRUCTION_SET_NAME;
|
|
129
|
+
const modelConfigIdentity = environment.ORIZU_MODEL_CONFIG_IDENTITY;
|
|
130
|
+
if (!instructionSetName || !modelConfigIdentity)
|
|
131
|
+
return undefined;
|
|
132
|
+
const rawVersion = environment.ORIZU_INSTRUCTION_SET_PROFILE_VERSION_NUMBER;
|
|
133
|
+
const versionNumber = rawVersion === undefined ? undefined : profileVersionNumber(rawVersion);
|
|
134
|
+
if (versionNumber === undefined && unversioned === 'server')
|
|
135
|
+
return undefined;
|
|
136
|
+
return resolveGepaInstructionSetProfileVersion(instructionSetName, modelConfigIdentity, project, versionNumber);
|
|
137
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"optimizations-promote-cli.d.ts","sourceRoot":"","sources":["../src/optimizations-promote-cli.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"optimizations-promote-cli.d.ts","sourceRoot":"","sources":["../src/optimizations-promote-cli.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAGpD,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,OAAO,CAAA;IACb,MAAM,EAAE,WAAW,CAAA;IACnB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC7B,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,kBAAkB,EAAE,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAClE,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;CACjE;AAqED,wBAAsB,0BAA0B,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD1G"}
|
|
@@ -54,6 +54,17 @@ function productionPromotionReplay(runId, candidateId, project, origin) {
|
|
|
54
54
|
: '';
|
|
55
55
|
return `Promote this profile with: orizu${originArguments} optimizations promote ${shellArgument(runId)} --candidate ${shellArgument(candidateId)} --label production --project ${shellArgument(project)}`;
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* ORI-2128: a candidate grown from a Seed older than Production does not
|
|
59
|
+
* contain the changes made since that Seed. Warn; never refuse (ORI-2132).
|
|
60
|
+
*/
|
|
61
|
+
function staleSeedWarningFor(data) {
|
|
62
|
+
const seed = data.runSeedVersionNumber;
|
|
63
|
+
const production = data.productionVersionNumber;
|
|
64
|
+
if (typeof seed !== 'number' || typeof production !== 'number' || seed >= production)
|
|
65
|
+
return null;
|
|
66
|
+
return `Warning: this run started from v${seed}; production is v${production}. Changes made since v${seed} are not in this candidate.`;
|
|
67
|
+
}
|
|
57
68
|
export async function promoteOptimizationCommand(args, io) {
|
|
58
69
|
const runId = args.find(arg => !arg.startsWith('--') && arg !== option(args, '--candidate') && arg !== option(args, '--label') && arg !== option(args, '--project'));
|
|
59
70
|
const candidateId = option(args, '--candidate');
|
|
@@ -72,15 +83,21 @@ export async function promoteOptimizationCommand(args, io) {
|
|
|
72
83
|
const replay = isProfilePromotion && data.appliedLabel !== 'production'
|
|
73
84
|
? productionPromotionReplay(runId, candidateId, project, io.origin)
|
|
74
85
|
: null;
|
|
86
|
+
const staleSeedWarning = isProfilePromotion ? staleSeedWarningFor(data) : null;
|
|
75
87
|
if (io.json) {
|
|
76
88
|
io.print(JSON.stringify(isProfilePromotion
|
|
77
89
|
? {
|
|
78
90
|
profileVersionId: data.profileVersionId,
|
|
79
91
|
versionNumber: data.versionNumber,
|
|
80
92
|
appliedLabel: data.appliedLabel,
|
|
93
|
+
// JSON.stringify omits these when an older server did not send them.
|
|
94
|
+
runSeedVersionNumber: data.runSeedVersionNumber,
|
|
95
|
+
productionVersionNumber: data.productionVersionNumber,
|
|
81
96
|
components: data.components,
|
|
82
97
|
}
|
|
83
98
|
: { promptVersionId: data.promptVersionId }));
|
|
99
|
+
if (staleSeedWarning)
|
|
100
|
+
io.printErr(staleSeedWarning);
|
|
84
101
|
if (isProfilePromotion && data.appliedLabel === undefined) {
|
|
85
102
|
io.printErr(APPLIED_LABEL_VERSION_SKEW_NOTE);
|
|
86
103
|
io.printErr(replay);
|
|
@@ -88,6 +105,8 @@ export async function promoteOptimizationCommand(args, io) {
|
|
|
88
105
|
return;
|
|
89
106
|
}
|
|
90
107
|
if (isProfilePromotion) {
|
|
108
|
+
if (staleSeedWarning)
|
|
109
|
+
io.printErr(staleSeedWarning);
|
|
91
110
|
io.print(`Promoted profile version ${sanitizeTerminalText(data.profileVersionId)}`);
|
|
92
111
|
if (data.appliedLabel === undefined) {
|
|
93
112
|
io.printErr(APPLIED_LABEL_VERSION_SKEW_NOTE);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORI-2128: what `optimizations promote` needs to warn about a stale seed.
|
|
3
|
+
* The run's seed version number, and its profile's production version number
|
|
4
|
+
* as it stood before this promotion (null when the profile had none).
|
|
5
|
+
* Shared by the promote handler, both promote routes, and the CLI.
|
|
6
|
+
*/
|
|
7
|
+
export interface SeedAgainstProduction {
|
|
8
|
+
runSeedVersionNumber: number;
|
|
9
|
+
productionVersionNumber: number | null;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=seed-against-production.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seed-against-production.d.ts","sourceRoot":"","sources":["../src/seed-against-production.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC,oBAAoB,EAAE,MAAM,CAAA;IAC5B,uBAAuB,EAAE,MAAM,GAAG,IAAI,CAAA;CACvC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'fs'
|
|
2
2
|
|
|
3
|
+
import { gepaSeedSelector } from './instruction-set-gepa-launch.js'
|
|
3
4
|
import { providerFromIdentity, protocolRequiresOutputCap } from './provider-registry.js'
|
|
4
5
|
import { normalizedSkilledProposerConfig } from './skilled-proposer-config.js'
|
|
5
6
|
|
|
@@ -58,6 +59,7 @@ const VALUE_OPTIONS: EnvironmentOption[] = [
|
|
|
58
59
|
{ flag: '--log-dir', environment: 'ORIZU_LOCAL_LOG_DIR' },
|
|
59
60
|
{ flag: '--instruction-set', environment: 'ORIZU_INSTRUCTION_SET_NAME' },
|
|
60
61
|
{ flag: '--model-config', environment: 'ORIZU_MODEL_CONFIG_IDENTITY' },
|
|
62
|
+
{ flag: '--profile-version', environment: 'ORIZU_INSTRUCTION_SET_PROFILE_VERSION_NUMBER' },
|
|
61
63
|
]
|
|
62
64
|
|
|
63
65
|
const BOOLEAN_OPTIONS = [
|
|
@@ -301,6 +303,21 @@ function translateOfficialOptions(args: string[], project: string, environment:
|
|
|
301
303
|
throw new Error('Budget options are mutually exclusive; choose at most one of --budget, --max-metric-calls, --max-full-evals, --max-iterations, --max-candidate-proposals')
|
|
302
304
|
}
|
|
303
305
|
if (hasInstructionSet && hasCandidateVersion) throw new Error('--instruction-set and --candidate-version-id are mutually exclusive')
|
|
306
|
+
if (!hasInstructionSet && connectorEnvironment.ORIZU_INSTRUCTION_SET_PROFILE_VERSION_NUMBER !== undefined) {
|
|
307
|
+
throw new Error('--profile-version requires --instruction-set')
|
|
308
|
+
}
|
|
309
|
+
if (hasInstructionSet) {
|
|
310
|
+
// ORI-2128: the Seed may be named by a full specifier, by flags, or both;
|
|
311
|
+
// normalise to the set reference, the profile, and the version number.
|
|
312
|
+
const seed = gepaSeedSelector(
|
|
313
|
+
connectorEnvironment.ORIZU_INSTRUCTION_SET_NAME!,
|
|
314
|
+
connectorEnvironment.ORIZU_MODEL_CONFIG_IDENTITY,
|
|
315
|
+
connectorEnvironment.ORIZU_INSTRUCTION_SET_PROFILE_VERSION_NUMBER,
|
|
316
|
+
)
|
|
317
|
+
connectorEnvironment.ORIZU_INSTRUCTION_SET_NAME = seed.instructionSetName
|
|
318
|
+
if (seed.modelConfigIdentity !== undefined) connectorEnvironment.ORIZU_MODEL_CONFIG_IDENTITY = seed.modelConfigIdentity
|
|
319
|
+
if (seed.versionNumber !== undefined) connectorEnvironment.ORIZU_INSTRUCTION_SET_PROFILE_VERSION_NUMBER = String(seed.versionNumber)
|
|
320
|
+
}
|
|
304
321
|
if (hasInstructionSet && connectorEnvironment.ORIZU_MODEL_CONFIG_IDENTITY === undefined) throw new Error('--instruction-set requires --model-config')
|
|
305
322
|
if (hasInstructionSet && connectorEnvironment.ORIZU_COMPONENT_SELECTOR === undefined) connectorEnvironment.ORIZU_COMPONENT_SELECTOR = 'round_robin'
|
|
306
323
|
if ((connectorEnvironment.ORIZU_PROPOSAL_MAX_CALLS !== undefined
|
|
@@ -355,7 +372,7 @@ function validatedMetadata(raw: string | undefined): string {
|
|
|
355
372
|
export function dispatchGepaEngine(args: string[], project: string, environment: NodeJS.ProcessEnv): GepaEngineDispatch {
|
|
356
373
|
const selected = removeEngine(args)
|
|
357
374
|
if (selected.engine === 'legacy') {
|
|
358
|
-
for (const flag of ['--instruction-set', '--model-config', '--component-selector', '--max-candidate-proposals', '--candidate-proposer', '--candidate-proposer-config', '--proposal-max-calls', '--proposal-max-tokens']) {
|
|
375
|
+
for (const flag of ['--instruction-set', '--model-config', '--profile-version', '--component-selector', '--max-candidate-proposals', '--candidate-proposer', '--candidate-proposer-config', '--proposal-max-calls', '--proposal-max-tokens']) {
|
|
359
376
|
if (selected.args.some(argument => argument === flag || argument.startsWith(`${flag}=`))) {
|
|
360
377
|
throw new Error(`${flag} is supported by the official GEPA engine only`)
|
|
361
378
|
}
|
package/src/help.ts
CHANGED
|
@@ -682,8 +682,8 @@ export const COMMAND_DOCS: CliCommandDoc[] = [
|
|
|
682
682
|
},
|
|
683
683
|
{
|
|
684
684
|
path: ['optimizations', 'run-gepa'],
|
|
685
|
-
usage: 'orizu optimizations run-gepa [--hosted [--launch-intent-id <uuid>]] --project <team/project> --optimizer-version-id <id> (--candidate-version-id <id> | --instruction-set <
|
|
686
|
-
summary: 'Run official GEPA locally and stream events, or use --hosted for a queued Orizu-managed sandbox launch. Hosted eligibility requires a staff-enabled team, capacity below its concurrent-run cap, a launch-project optimizer version whose validated manifest.optimizer_family is gepa, registered candidate/scorer runners, a reflection model from a registered provider, and a named --budget preset within the team ceiling. Hosted eligibility catalog refusals use stable code/remediation JSON with optional bounded detail, and capacity refusals include monitor URLs for running runs. A successful launch prints a durable monitor URL; queued means accepted, not completed. Hosted mode does not require --candidate-runner-dir or --scorer-runner-dir; when supplied, those directories must byte-match their registered versions. Retry response loss with the same --launch-intent-id. Local runner version ids materialize registered bytes, while optional runner dirs are additionally byte-verified before execution. Pass --engine legacy only for the frozen safety hatch. Skilled-proposer is official-only, its config requires that selection, and the selection cannot be combined with --reflection-prompt-template. --instruction-set
|
|
685
|
+
usage: 'orizu optimizations run-gepa [--hosted [--launch-intent-id <uuid>]] --project <team/project> --optimizer-version-id <id> (--candidate-version-id <id> | --instruction-set <set[/profile[@vN]]> [--model-config <identity>] [--profile-version <N>] [--component-selector round-robin|all]) --runner-version-id <id> [--candidate-runner-dir <dir>] --scorer-version-id <id> --scorer-runner-version-id <id> [--scorer-runner-dir <dir>] [--engine official|legacy] [--candidate-proposer skilled-proposer [--candidate-proposer-config <@file|path>] [--proposal-max-calls <n>] [--proposal-max-tokens <n>]] [--scorer-input-contract gepa|flat_row] [--scorer-candidate-field <row-field>] [--allow-degenerate-seed] [--seed <n>] --dataset-version-id <id> --split-set-id <id> [--train-split train] [--val-split validation] [--minibatch-size <n>] [--candidate-selection-strategy pareto|current_best|epsilon_greedy] [--epsilon <n>] [--objective <text>] [--budget auto|light|medium|heavy | --max-metric-calls <n> | --max-full-evals <n> | --max-iterations <n> | --max-candidate-proposals <n>] [--num-threads auto|N] [--reflection-model <provider/model>] [--reflection-temperature <n>] [--reflection-prompt-template <text|@file>] [--reflection-max-tokens N] [--reflection-provider-settings <json|@file>] [--reflection-retry-attempts N] [--reflection-http-timeout-seconds N] [--skip-perfect-parent-reflection|--no-skip-perfect-parent-reflection] [--disable-evaluation-cache] [--auto-promote] [--promotion-label <label>] [--log-row-snapshots] [--metadata <json>] [--python <command>] [--log-dir logs] [--no-local-log] [--json]',
|
|
686
|
+
summary: 'Run official GEPA locally and stream events, or use --hosted for a queued Orizu-managed sandbox launch. Hosted eligibility requires a staff-enabled team, capacity below its concurrent-run cap, a launch-project optimizer version whose validated manifest.optimizer_family is gepa, registered candidate/scorer runners, a reflection model from a registered provider, and a named --budget preset within the team ceiling. Hosted eligibility catalog refusals use stable code/remediation JSON with optional bounded detail, and capacity refusals include monitor URLs for running runs. A successful launch prints a durable monitor URL; queued means accepted, not completed. Hosted mode does not require --candidate-runner-dir or --scorer-runner-dir; when supplied, those directories must byte-match their registered versions. Retry response loss with the same --launch-intent-id. Local runner version ids materialize registered bytes, while optional runner dirs are additionally byte-verified before execution. Pass --engine legacy only for the frozen safety hatch. Skilled-proposer is official-only, its config requires that selection, and the selection cannot be combined with --reflection-prompt-template. --instruction-set needs a profile, named by --model-config or a set/profile specifier, and cannot be combined with --candidate-version-id. The run starts from the production version of that profile unless @vN or --profile-version <N> names another version (the seed, promoted or not); a specifier and flags that name different values are refused; round-robin updates one component-map entry per round and all updates every component. --max-candidate-proposals is official-engine-only. Validates the scorer contract on the seed before iterating; judge runners built for flat-row score runs need --scorer-input-contract flat_row (ALI-1158).',
|
|
687
687
|
group: 'Optimizations',
|
|
688
688
|
},
|
|
689
689
|
{
|
package/src/hosted-gepa-cli.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto'
|
|
2
2
|
|
|
3
3
|
import { dispatchGepaEngine } from './gepa-engine-dispatch.js'
|
|
4
|
+
import { resolveGepaSeedFromEnvironment } from './instruction-set-gepa-launch.js'
|
|
4
5
|
import { parseHostedGepaNumericValue } from './hosted-gepa-numeric-values.js'
|
|
5
6
|
import { authedFetch } from './http.js'
|
|
6
7
|
import { hostedProviderFromModel, hostedProviderSettingsError, unsupportedHostedProviderMessage } from './hosted-provider-settings.js'
|
|
@@ -216,11 +217,16 @@ export async function runHostedGepaOptimization(options: HostedGepaOptions): Pro
|
|
|
216
217
|
: kind === 'json' ? JSON.parse(env[name]!) : kind === 'boolean' ? env[name] === '1' : env[name]
|
|
217
218
|
}
|
|
218
219
|
const launchIntentId = explicitLaunchIntent || randomUUID()
|
|
220
|
+
// ORI-2128: a named Seed version is resolved here exactly as a local run
|
|
221
|
+
// resolves it, and pinned in the body; without one the server keeps its
|
|
222
|
+
// one-call production lookup.
|
|
223
|
+
const seedProfileVersionId = await resolveGepaSeedFromEnvironment(env, options.project, 'server')
|
|
219
224
|
const body = {
|
|
220
225
|
hosted: true, launchIntentId, optimizerVersionId: env.ORIZU_OPTIMIZER_VERSION_ID,
|
|
221
226
|
promptVersionIds: env.ORIZU_PROMPT_VERSION_ID ? [env.ORIZU_PROMPT_VERSION_ID] : [],
|
|
222
227
|
...(env.ORIZU_INSTRUCTION_SET_NAME ? { instructionSetName: env.ORIZU_INSTRUCTION_SET_NAME,
|
|
223
|
-
modelConfigIdentity: env.ORIZU_MODEL_CONFIG_IDENTITY, componentSelector: env.ORIZU_COMPONENT_SELECTOR
|
|
228
|
+
modelConfigIdentity: env.ORIZU_MODEL_CONFIG_IDENTITY, componentSelector: env.ORIZU_COMPONENT_SELECTOR,
|
|
229
|
+
...(seedProfileVersionId ? { instructionSetProfileVersionId: seedProfileVersionId } : {}) } : {}),
|
|
224
230
|
runnerVersionId: env.ORIZU_RUNNER_VERSION_ID,
|
|
225
231
|
scorers: [{ scorerVersionId: env.ORIZU_SCORER_VERSION_ID,
|
|
226
232
|
scorerRunnerVersionId: env.ORIZU_SCORER_RUNNER_VERSION_ID, role: 'selection', scorerConfig: {} }],
|
|
@@ -95,6 +95,7 @@
|
|
|
95
95
|
|
|
96
96
|
import { parseModelIdentity } from './provider-registry.js'
|
|
97
97
|
|
|
98
|
+
import { HARNESS_NAMES } from './hosted-harness.js'
|
|
98
99
|
import type {
|
|
99
100
|
AgentHarness,
|
|
100
101
|
HarnessEvent,
|
|
@@ -323,7 +324,7 @@ export function createClaudeAgentHarness(
|
|
|
323
324
|
}
|
|
324
325
|
|
|
325
326
|
return {
|
|
326
|
-
name:
|
|
327
|
+
name: HARNESS_NAMES.claudeAgentSdk,
|
|
327
328
|
|
|
328
329
|
async start(opts: HarnessStartOptions): Promise<{ agentSessionId: string }> {
|
|
329
330
|
startModel = opts.model
|
|
@@ -471,6 +472,7 @@ export function createClaudeAgentHarness(
|
|
|
471
472
|
args: asRecord(block.input),
|
|
472
473
|
callId,
|
|
473
474
|
status: 'running',
|
|
475
|
+
harness: HARNESS_NAMES.claudeAgentSdk,
|
|
474
476
|
},
|
|
475
477
|
}
|
|
476
478
|
}
|
|
@@ -495,6 +497,7 @@ export function createClaudeAgentHarness(
|
|
|
495
497
|
callId,
|
|
496
498
|
status: block.is_error === true ? 'error' : 'completed',
|
|
497
499
|
output: toolResultOutput(block.content),
|
|
500
|
+
harness: HARNESS_NAMES.claudeAgentSdk,
|
|
498
501
|
},
|
|
499
502
|
}
|
|
500
503
|
}
|
|
@@ -53,11 +53,13 @@ import { readFileSync } from 'fs'
|
|
|
53
53
|
|
|
54
54
|
import { findProvider, parseModelIdentity } from './provider-registry.js'
|
|
55
55
|
|
|
56
|
+
import { HARNESS_NAMES } from './hosted-harness.js'
|
|
56
57
|
import type {
|
|
57
58
|
AgentHarness,
|
|
58
59
|
HarnessEvent,
|
|
59
60
|
HarnessPrompt,
|
|
60
61
|
HarnessStartOptions,
|
|
62
|
+
HarnessToolPayload,
|
|
61
63
|
} from './hosted-harness.js'
|
|
62
64
|
|
|
63
65
|
// OpenCode is HARD-PINNED to this version everywhere it is installed/launched.
|
|
@@ -121,7 +123,9 @@ class PromptAbortedError extends Error {
|
|
|
121
123
|
// -- OpenCode ascending id generator (port of OpenCode's id.ts) --------------
|
|
122
124
|
// Monotonic so our user-message id always sorts AFTER any prior assistant
|
|
123
125
|
// message id — this is what prevents OpenCode's prompt loop from early-exiting
|
|
124
|
-
// on `lastUser.id < lastAssistant.id`.
|
|
126
|
+
// on `lastUser.id < lastAssistant.id`. Shape (opencode-ai@1.14.41): prefix, `_`,
|
|
127
|
+
// 12 hex digits holding the low 48 bits of `ms * 4096 + counter`, then 14
|
|
128
|
+
// base62 characters. `nowMs` is a parameter only so tests can pin the clock.
|
|
125
129
|
const BASE62 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
|
126
130
|
let idLastTimestamp = 0
|
|
127
131
|
let idCounter = 0
|
|
@@ -134,14 +138,15 @@ function randomBase62(length: number): string {
|
|
|
134
138
|
return out
|
|
135
139
|
}
|
|
136
140
|
|
|
137
|
-
function ascendingId(prefix: 'ses' | 'msg' | 'prt'): string {
|
|
138
|
-
const nowMs = Date.now()
|
|
141
|
+
export function ascendingId(prefix: 'ses' | 'msg' | 'prt', nowMs: number = Date.now()): string {
|
|
139
142
|
if (nowMs !== idLastTimestamp) {
|
|
140
143
|
idLastTimestamp = nowMs
|
|
141
144
|
idCounter = 0
|
|
142
145
|
}
|
|
143
146
|
idCounter += 1
|
|
144
|
-
|
|
147
|
+
// BigInt, as OpenCode does: the JS `&` operator works on 32 bits, which made
|
|
148
|
+
// short, sometimes negative ids that sorted before OpenCode's own (ORI-2116).
|
|
149
|
+
const encoded = (BigInt(nowMs) * BigInt(0x1000) + BigInt(idCounter)) & BigInt(0xffffffffffff)
|
|
145
150
|
const hex = encoded.toString(16).padStart(12, '0')
|
|
146
151
|
return `${prefix}_${hex}${randomBase62(14)}`
|
|
147
152
|
}
|
|
@@ -495,7 +500,7 @@ export function createOpenCodeHarness(options: OpenCodeHarnessOptions): AgentHar
|
|
|
495
500
|
}
|
|
496
501
|
|
|
497
502
|
return {
|
|
498
|
-
name:
|
|
503
|
+
name: HARNESS_NAMES.opencode,
|
|
499
504
|
|
|
500
505
|
async start(opts: HarnessStartOptions): Promise<{ agentSessionId: string }> {
|
|
501
506
|
startModel = opts.model
|
|
@@ -598,13 +603,25 @@ export function createOpenCodeHarness(options: OpenCodeHarnessOptions): AgentHar
|
|
|
598
603
|
if (emittedToolStates.has(toolKey)) return events
|
|
599
604
|
emittedToolStates.add(toolKey)
|
|
600
605
|
const terminal = status === 'completed' || status === 'error'
|
|
601
|
-
const payload:
|
|
606
|
+
const payload: HarnessToolPayload = {
|
|
602
607
|
tool: typeof part.tool === 'string' ? part.tool : '',
|
|
603
608
|
args: input,
|
|
604
609
|
callId,
|
|
605
610
|
status,
|
|
611
|
+
harness: HARNESS_NAMES.opencode,
|
|
612
|
+
}
|
|
613
|
+
// A failed call keeps its message in `state.error`, not `state.output`
|
|
614
|
+
// (opencode-ai@1.14.41 ToolStateError); it is that call's output.
|
|
615
|
+
if (terminal) {
|
|
616
|
+
payload.output = typeof state.output === 'string' ? state.output
|
|
617
|
+
: typeof state.error === 'string' ? state.error : ''
|
|
618
|
+
}
|
|
619
|
+
// OpenCode's own one-line title and structured extras (ORI-2160),
|
|
620
|
+
// passed through as given; absent when the state has none.
|
|
621
|
+
if (typeof state.title === 'string' && state.title.length > 0) payload.title = state.title
|
|
622
|
+
if (state.metadata && typeof state.metadata === 'object' && !Array.isArray(state.metadata)) {
|
|
623
|
+
payload.metadata = state.metadata as Record<string, unknown>
|
|
606
624
|
}
|
|
607
|
-
if (terminal) payload.output = typeof state.output === 'string' ? state.output : ''
|
|
608
625
|
events.push({ kind: terminal ? 'tool_result' : 'tool_call', messageId: msgId, payload })
|
|
609
626
|
} else if (partType === 'step-start') {
|
|
610
627
|
events.push({ kind: 'step_start', messageId: msgId, payload: {} })
|
package/src/hosted-harness.ts
CHANGED
|
@@ -117,8 +117,39 @@ export interface HarnessEvent {
|
|
|
117
117
|
critical?: boolean
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/** The drivers' stable names. A tool event's `harness` field is one of these. */
|
|
121
|
+
export const HARNESS_NAMES = {
|
|
122
|
+
opencode: 'opencode',
|
|
123
|
+
claudeAgentSdk: 'claude-agent-sdk',
|
|
124
|
+
} as const
|
|
125
|
+
|
|
126
|
+
type HarnessName = (typeof HARNESS_NAMES)[keyof typeof HARNESS_NAMES]
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Payload of a `tool_call` / `tool_result` event (ORI-2160). One chat renderer
|
|
130
|
+
* shows every tool call whatever harness produced it, so the payload carries:
|
|
131
|
+
* • `title` — the harness's one-line summary of the call (OpenCode's
|
|
132
|
+
* `state.title`). Absent when the harness has none.
|
|
133
|
+
* • `metadata` — structured extras such as a diff, an exit code or match
|
|
134
|
+
* counts (OpenCode's `state.metadata`). Absent when none.
|
|
135
|
+
* • `harness` — which driver produced the event.
|
|
136
|
+
* All three are optional: an event without them is still a valid tool event.
|
|
137
|
+
* The sink and the digest pass them through unchanged.
|
|
138
|
+
*/
|
|
139
|
+
export interface HarnessToolPayload {
|
|
140
|
+
[key: string]: unknown
|
|
141
|
+
tool?: string
|
|
142
|
+
args?: Record<string, unknown>
|
|
143
|
+
callId: string
|
|
144
|
+
status: string
|
|
145
|
+
output?: string
|
|
146
|
+
title?: string
|
|
147
|
+
metadata?: Record<string, unknown>
|
|
148
|
+
harness?: HarnessName
|
|
149
|
+
}
|
|
150
|
+
|
|
120
151
|
export interface AgentHarness {
|
|
121
|
-
/** Stable driver name —
|
|
152
|
+
/** Stable driver name — one of HARNESS_NAMES. */
|
|
122
153
|
readonly name: string
|
|
123
154
|
/** Boot the agent process/session; returns the resumable agent session id. */
|
|
124
155
|
start(opts: HarnessStartOptions): Promise<{ agentSessionId: string }>
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool display hints (ORI-2160, moved here for ORI-2167).
|
|
3
|
+
*
|
|
4
|
+
* Each harness driver has its own built-in tools. This module holds, per
|
|
5
|
+
* driver, what the chat needs to show a call without switching on its name:
|
|
6
|
+
* a category (icon), a label (the verb on the row), the input key that
|
|
7
|
+
* summarizes the call, how to show the output, and input keys to hide.
|
|
8
|
+
*
|
|
9
|
+
* It has no imports with side effects, so both the CLI and the web app
|
|
10
|
+
* (the chat renderer, and the activity projection that picks which input
|
|
11
|
+
* key reaches the browser) import it directly.
|
|
12
|
+
*/
|
|
13
|
+
// Type-only, so the web build never has to load the harness module itself.
|
|
14
|
+
import type { HARNESS_NAMES } from './hosted-harness.js'
|
|
15
|
+
|
|
16
|
+
type HarnessNames = typeof HARNESS_NAMES
|
|
17
|
+
const OPENCODE: HarnessNames['opencode'] = 'opencode'
|
|
18
|
+
const CLAUDE_AGENT_SDK: HarnessNames['claudeAgentSdk'] = 'claude-agent-sdk'
|
|
19
|
+
|
|
20
|
+
/** What kind of thing a tool does — the renderer picks an icon/layout from
|
|
21
|
+
* this, never from the tool name. */
|
|
22
|
+
export const TOOL_CATEGORIES = {
|
|
23
|
+
file: 'file',
|
|
24
|
+
command: 'command',
|
|
25
|
+
search: 'search',
|
|
26
|
+
network: 'network',
|
|
27
|
+
question: 'question',
|
|
28
|
+
subAgent: 'sub-agent',
|
|
29
|
+
list: 'list',
|
|
30
|
+
plug: 'plug',
|
|
31
|
+
other: 'other',
|
|
32
|
+
} as const
|
|
33
|
+
|
|
34
|
+
export type ToolCategory = (typeof TOOL_CATEGORIES)[keyof typeof TOOL_CATEGORIES]
|
|
35
|
+
|
|
36
|
+
/** How the tool's output text should be shown. */
|
|
37
|
+
export const TOOL_OUTPUT_KINDS = {
|
|
38
|
+
text: 'text',
|
|
39
|
+
diff: 'diff',
|
|
40
|
+
markdown: 'markdown',
|
|
41
|
+
json: 'json',
|
|
42
|
+
list: 'list',
|
|
43
|
+
} as const
|
|
44
|
+
|
|
45
|
+
type ToolOutputKind = (typeof TOOL_OUTPUT_KINDS)[keyof typeof TOOL_OUTPUT_KINDS]
|
|
46
|
+
|
|
47
|
+
/** Per-tool display hint. Each driver owns a table of these for its built-in
|
|
48
|
+
* tools, so tool-name knowledge lives behind the seam. */
|
|
49
|
+
export interface ToolHint {
|
|
50
|
+
category: ToolCategory
|
|
51
|
+
/** The verb the chat row shows ("Read", "Ran"). A tool without a hint shows
|
|
52
|
+
* its own name instead. */
|
|
53
|
+
label?: string
|
|
54
|
+
/** The input key whose value summarizes the call (e.g. `command`), or null
|
|
55
|
+
* when the tool takes no input worth summarizing. */
|
|
56
|
+
summaryKey: string | null
|
|
57
|
+
outputKind: ToolOutputKind
|
|
58
|
+
/** Input keys whose values must not be shown as-is (large bodies such as a
|
|
59
|
+
* file's full new content, or likely secrets). */
|
|
60
|
+
redactKeys: readonly string[]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type ToolHintTable = Readonly<Record<string, ToolHint>>
|
|
64
|
+
|
|
65
|
+
// Argument names that look like they hold a credential. Redacted for every
|
|
66
|
+
// tool, and never used as the fallback summary.
|
|
67
|
+
const SECRET_LOOKING_KEY = /token|secret|password|passwd|api[_-]?key|authorization|credential/i
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Look a tool up in a driver's hint table. A tool the table does not know (an
|
|
71
|
+
* MCP tool, a plug-in) falls back to the `plug` category, summarized by its
|
|
72
|
+
* first non-empty string argument that does not look like a secret.
|
|
73
|
+
*/
|
|
74
|
+
export function resolveToolHint(
|
|
75
|
+
table: ToolHintTable,
|
|
76
|
+
tool: string,
|
|
77
|
+
args: Record<string, unknown> = {}
|
|
78
|
+
): ToolHint {
|
|
79
|
+
const known = Object.hasOwn(table, tool) ? table[tool] : undefined
|
|
80
|
+
const secretKeys = Object.keys(args).filter(key => SECRET_LOOKING_KEY.test(key))
|
|
81
|
+
const redactKeys = [...new Set([...(known?.redactKeys ?? []), ...secretKeys])]
|
|
82
|
+
if (known) return { ...known, redactKeys }
|
|
83
|
+
const summaryKey =
|
|
84
|
+
Object.keys(args).find(key => {
|
|
85
|
+
const value = args[key]
|
|
86
|
+
return typeof value === 'string' && value.length > 0 && !redactKeys.includes(key)
|
|
87
|
+
}) ?? null
|
|
88
|
+
return { category: TOOL_CATEGORIES.plug, summaryKey, outputKind: TOOL_OUTPUT_KINDS.text, redactKeys }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// OpenCode's built-in tools, as the pinned opencode-ai@1.14.41 tool registry
|
|
92
|
+
// lists them, with the input key that summarizes each call. Tools not listed
|
|
93
|
+
// here (MCP tools, plug-ins) resolve to `plug` through resolveToolHint.
|
|
94
|
+
export const OPENCODE_TOOL_HINTS: ToolHintTable = {
|
|
95
|
+
read: { category: 'file', label: 'Read', summaryKey: 'filePath', outputKind: 'text', redactKeys: [] },
|
|
96
|
+
edit: { category: 'file', label: 'Edited', summaryKey: 'filePath', outputKind: 'diff', redactKeys: ['oldString', 'newString'] },
|
|
97
|
+
write: { category: 'file', label: 'Wrote', summaryKey: 'filePath', outputKind: 'text', redactKeys: ['content'] },
|
|
98
|
+
apply_patch: { category: 'file', label: 'Edited', summaryKey: 'patchText', outputKind: 'diff', redactKeys: [] },
|
|
99
|
+
lsp: { category: 'file', label: 'Checked', summaryKey: 'filePath', outputKind: 'json', redactKeys: [] },
|
|
100
|
+
bash: { category: 'command', label: 'Ran', summaryKey: 'command', outputKind: 'text', redactKeys: [] },
|
|
101
|
+
glob: { category: 'search', label: 'Listed', summaryKey: 'pattern', outputKind: 'list', redactKeys: [] },
|
|
102
|
+
grep: { category: 'search', label: 'Searched', summaryKey: 'pattern', outputKind: 'text', redactKeys: [] },
|
|
103
|
+
webfetch: { category: 'network', label: 'Fetched', summaryKey: 'url', outputKind: 'markdown', redactKeys: [] },
|
|
104
|
+
websearch: { category: 'network', label: 'Searched', summaryKey: 'query', outputKind: 'text', redactKeys: [] },
|
|
105
|
+
question: { category: 'question', label: 'Asked', summaryKey: 'questions', outputKind: 'text', redactKeys: [] },
|
|
106
|
+
plan_exit: { category: 'question', label: 'Asked', summaryKey: null, outputKind: 'text', redactKeys: [] },
|
|
107
|
+
task: { category: 'sub-agent', label: 'Sub-agent', summaryKey: 'description', outputKind: 'markdown', redactKeys: [] },
|
|
108
|
+
todowrite: { category: 'list', label: 'Planned', summaryKey: 'todos', outputKind: 'json', redactKeys: [] },
|
|
109
|
+
skill: { category: 'other', label: 'Loaded', summaryKey: 'name', outputKind: 'markdown', redactKeys: [] },
|
|
110
|
+
invalid: { category: 'other', label: 'Invalid', summaryKey: 'tool', outputKind: 'text', redactKeys: [] },
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// The Claude Agent SDK's built-in tools (SDK 0.3.201: input types in
|
|
114
|
+
// sdk-tools.d.ts, names from the SDK; `Task` is the older name of `Agent`),
|
|
115
|
+
// with the input key that summarizes each call. MCP tools
|
|
116
|
+
// (`mcp__<server>__<tool>`) are not listed and resolve to `plug` through
|
|
117
|
+
// resolveToolHint. This driver sends no `title` or `metadata`: the SDK has no
|
|
118
|
+
// equivalent of OpenCode's.
|
|
119
|
+
export const CLAUDE_TOOL_HINTS: ToolHintTable = {
|
|
120
|
+
Read: { category: 'file', label: 'Read', summaryKey: 'file_path', outputKind: 'text', redactKeys: [] },
|
|
121
|
+
Edit: { category: 'file', label: 'Edited', summaryKey: 'file_path', outputKind: 'text', redactKeys: ['old_string', 'new_string'] },
|
|
122
|
+
Write: { category: 'file', label: 'Wrote', summaryKey: 'file_path', outputKind: 'text', redactKeys: ['content'] },
|
|
123
|
+
NotebookEdit: { category: 'file', label: 'Edited', summaryKey: 'notebook_path', outputKind: 'text', redactKeys: ['new_source'] },
|
|
124
|
+
Bash: { category: 'command', label: 'Ran', summaryKey: 'command', outputKind: 'text', redactKeys: [] },
|
|
125
|
+
TaskOutput: { category: 'command', label: 'Checked', summaryKey: 'task_id', outputKind: 'text', redactKeys: [] },
|
|
126
|
+
TaskStop: { category: 'command', label: 'Stopped', summaryKey: 'task_id', outputKind: 'text', redactKeys: [] },
|
|
127
|
+
Glob: { category: 'search', label: 'Listed', summaryKey: 'pattern', outputKind: 'list', redactKeys: [] },
|
|
128
|
+
Grep: { category: 'search', label: 'Searched', summaryKey: 'pattern', outputKind: 'text', redactKeys: [] },
|
|
129
|
+
WebFetch: { category: 'network', label: 'Fetched', summaryKey: 'url', outputKind: 'markdown', redactKeys: [] },
|
|
130
|
+
WebSearch: { category: 'network', label: 'Searched', summaryKey: 'query', outputKind: 'text', redactKeys: [] },
|
|
131
|
+
AskUserQuestion: { category: 'question', label: 'Asked', summaryKey: 'questions', outputKind: 'text', redactKeys: [] },
|
|
132
|
+
ExitPlanMode: { category: 'question', label: 'Asked', summaryKey: null, outputKind: 'markdown', redactKeys: [] },
|
|
133
|
+
Agent: { category: 'sub-agent', label: 'Sub-agent', summaryKey: 'description', outputKind: 'markdown', redactKeys: [] },
|
|
134
|
+
Task: { category: 'sub-agent', label: 'Sub-agent', summaryKey: 'description', outputKind: 'markdown', redactKeys: [] },
|
|
135
|
+
TodoWrite: { category: 'list', label: 'Planned', summaryKey: 'todos', outputKind: 'text', redactKeys: [] },
|
|
136
|
+
ListMcpResourcesTool: { category: 'plug', label: 'Listed', summaryKey: 'server', outputKind: 'json', redactKeys: [] },
|
|
137
|
+
ReadMcpResourceTool: { category: 'plug', label: 'Read', summaryKey: 'uri', outputKind: 'text', redactKeys: [] },
|
|
138
|
+
Skill: { category: 'other', label: 'Loaded', summaryKey: 'skill', outputKind: 'markdown', redactKeys: [] },
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const NO_HINTS: ToolHintTable = {}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The hint table for the harness that produced an event. Events recorded
|
|
145
|
+
* before the seam named its harness came from OpenCode, the default driver.
|
|
146
|
+
* A harness with no table here (a future pi driver, say) gets none, so every
|
|
147
|
+
* tool falls back to its own name and first string argument.
|
|
148
|
+
*/
|
|
149
|
+
export function toolHintsFor(harness: string | undefined): ToolHintTable {
|
|
150
|
+
if (harness === undefined || harness === OPENCODE) return OPENCODE_TOOL_HINTS
|
|
151
|
+
if (harness === CLAUDE_AGENT_SDK) return CLAUDE_TOOL_HINTS
|
|
152
|
+
return NO_HINTS
|
|
153
|
+
}
|