@zhivex-ai/harness 0.6.1 → 0.8.0
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/CHANGELOG.md +43 -0
- package/README.md +50 -16
- package/ROADMAP.md +69 -19
- package/SUPPORT.md +2 -2
- package/dist/cli-stream.d.ts +167 -0
- package/dist/cli.d.ts +18 -27
- package/dist/cli.js +1867 -287
- package/dist/config.d.ts +8 -38
- package/dist/harness.d.ts +5 -2
- package/dist/hostile-repository-demo.js +398 -147
- package/dist/index.d.ts +9 -3
- package/dist/index.js +1424 -369
- package/dist/providers.d.ts +77 -0
- package/dist/routing.d.ts +16 -0
- package/dist/sessions.d.ts +122 -0
- package/dist/zhx.js +6911 -0
- package/docs/CLI.md +37 -2
- package/docs/DURABLE_OPERATIONS.md +37 -0
- package/docs/EXTENSIBILITY.md +37 -1
- package/docs/LIVE_CERTIFICATION.md +44 -129
- package/docs/RELEASE.md +10 -25
- package/package.json +11 -8
package/dist/cli.js
CHANGED
|
@@ -3,21 +3,369 @@
|
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
5
|
import { createInterface } from "readline/promises";
|
|
6
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
6
7
|
import { constants as fsConstants } from "fs";
|
|
7
|
-
import { access, lstat as
|
|
8
|
-
import
|
|
8
|
+
import { access, lstat as lstat7, readFile as readFile4, stat as stat4 } from "fs/promises";
|
|
9
|
+
import path9 from "path";
|
|
9
10
|
import {
|
|
10
11
|
getAgentBudgetStatus as getAgentBudgetStatus3
|
|
11
12
|
} from "@zhivex-ai/agents";
|
|
12
13
|
|
|
13
14
|
// src/config.ts
|
|
14
|
-
import { createHash } from "crypto";
|
|
15
|
+
import { createHash as createHash2 } from "crypto";
|
|
15
16
|
import { realpathSync } from "fs";
|
|
16
17
|
import path from "path";
|
|
18
|
+
|
|
19
|
+
// src/providers.ts
|
|
20
|
+
import { createHash } from "crypto";
|
|
21
|
+
import { createGemini } from "@zhivex-ai/gemini";
|
|
17
22
|
import { createMeta } from "@zhivex-ai/meta";
|
|
18
23
|
import { createOpenAI } from "@zhivex-ai/openai";
|
|
19
24
|
import { createQwen } from "@zhivex-ai/qwen";
|
|
20
|
-
var PROVIDERS = ["meta", "qwen", "openai"];
|
|
25
|
+
var PROVIDERS = ["meta", "qwen", "openai", "gemini"];
|
|
26
|
+
var HARNESS_PROVIDER_CAPABILITIES = [
|
|
27
|
+
"streaming",
|
|
28
|
+
"tool-calling",
|
|
29
|
+
"approval-resume"
|
|
30
|
+
];
|
|
31
|
+
var QWEN_REGIONS = [
|
|
32
|
+
"singapore",
|
|
33
|
+
"beijing",
|
|
34
|
+
"hong-kong",
|
|
35
|
+
"tokyo",
|
|
36
|
+
"frankfurt",
|
|
37
|
+
"virginia"
|
|
38
|
+
];
|
|
39
|
+
var PROVIDER_ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
40
|
+
var ENVIRONMENT_VARIABLE = /^[A-Z_][A-Z0-9_]{0,127}$/;
|
|
41
|
+
var DIAGNOSTIC_KEY = /^[a-z][A-Za-z0-9]{0,63}$/;
|
|
42
|
+
var requiredText = (name, value) => {
|
|
43
|
+
const normalized = value.trim();
|
|
44
|
+
if (!normalized || /[\u0000-\u001f\u007f]/.test(normalized)) {
|
|
45
|
+
throw new Error(`${name} must be a non-empty printable string.`);
|
|
46
|
+
}
|
|
47
|
+
return normalized;
|
|
48
|
+
};
|
|
49
|
+
var environmentVariable = (name, value) => {
|
|
50
|
+
if (!ENVIRONMENT_VARIABLE.test(value)) {
|
|
51
|
+
throw new Error(`${name} must be a valid environment variable name.`);
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
};
|
|
55
|
+
var diagnosticKey = (value) => {
|
|
56
|
+
if (!DIAGNOSTIC_KEY.test(value)) {
|
|
57
|
+
throw new Error(`Invalid provider diagnostic key: ${value}.`);
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
};
|
|
61
|
+
var normalizedDescriptor = (descriptor) => {
|
|
62
|
+
const id = descriptor.id.trim().toLowerCase();
|
|
63
|
+
if (descriptor.id !== id || !PROVIDER_ID.test(id)) {
|
|
64
|
+
throw new Error(`Invalid provider id: ${descriptor.id}. Use 1-64 lowercase letters, digits, dot, underscore, or hyphen.`);
|
|
65
|
+
}
|
|
66
|
+
if (!(descriptor.support === "certified" || descriptor.support === "provisional")) {
|
|
67
|
+
throw new Error(`Invalid support level for provider ${id}.`);
|
|
68
|
+
}
|
|
69
|
+
const credentialNames = [...new Set(descriptor.credentialNames.map((name) => environmentVariable(`Credential for provider ${id}`, name)))];
|
|
70
|
+
const capabilities = [...new Set(descriptor.capabilities)];
|
|
71
|
+
const invalidCapability = capabilities.find((capability) => !HARNESS_PROVIDER_CAPABILITIES.includes(capability));
|
|
72
|
+
if (invalidCapability) {
|
|
73
|
+
throw new Error(`Invalid capability for provider ${id}: ${invalidCapability}.`);
|
|
74
|
+
}
|
|
75
|
+
return Object.freeze({
|
|
76
|
+
id,
|
|
77
|
+
name: requiredText(`Name for provider ${id}`, descriptor.name),
|
|
78
|
+
defaultModel: requiredText(`Default model for provider ${id}`, descriptor.defaultModel),
|
|
79
|
+
credentialNames: Object.freeze(credentialNames),
|
|
80
|
+
capabilities: Object.freeze(capabilities),
|
|
81
|
+
support: descriptor.support
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
var normalizedDiagnostics = (provider, diagnostics) => {
|
|
85
|
+
const endpointEnvironmentVariable = diagnostics?.endpointEnvironmentVariable === undefined ? undefined : environmentVariable(`Endpoint diagnostic for provider ${provider}`, diagnostics.endpointEnvironmentVariable);
|
|
86
|
+
const seenKeys = new Set;
|
|
87
|
+
const presence = (diagnostics?.presence ?? []).map((entry) => {
|
|
88
|
+
const key = diagnosticKey(entry.key);
|
|
89
|
+
if (seenKeys.has(key)) {
|
|
90
|
+
throw new Error(`Duplicate diagnostic key for provider ${provider}: ${key}.`);
|
|
91
|
+
}
|
|
92
|
+
seenKeys.add(key);
|
|
93
|
+
return Object.freeze({
|
|
94
|
+
key,
|
|
95
|
+
environmentVariable: environmentVariable(`Presence diagnostic for provider ${provider}`, entry.environmentVariable)
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
const enums = (diagnostics?.enums ?? []).map((entry) => {
|
|
99
|
+
const key = diagnosticKey(entry.key);
|
|
100
|
+
if (seenKeys.has(key)) {
|
|
101
|
+
throw new Error(`Duplicate diagnostic key for provider ${provider}: ${key}.`);
|
|
102
|
+
}
|
|
103
|
+
seenKeys.add(key);
|
|
104
|
+
const allowedValues = [...new Set(entry.allowedValues.map((value) => requiredText(`Allowed diagnostic value for provider ${provider}`, value)))];
|
|
105
|
+
if (allowedValues.length === 0) {
|
|
106
|
+
throw new Error(`Enum diagnostic ${key} for provider ${provider} needs allowed values.`);
|
|
107
|
+
}
|
|
108
|
+
return Object.freeze({
|
|
109
|
+
key,
|
|
110
|
+
environmentVariable: environmentVariable(`Enum diagnostic for provider ${provider}`, entry.environmentVariable),
|
|
111
|
+
allowedValues: Object.freeze(allowedValues)
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
return Object.freeze({
|
|
115
|
+
...endpointEnvironmentVariable ? { endpointEnvironmentVariable } : {},
|
|
116
|
+
...presence.length > 0 ? { presence: Object.freeze(presence) } : {},
|
|
117
|
+
...enums.length > 0 ? { enums: Object.freeze(enums) } : {}
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
var endpointConfiguration = (value) => {
|
|
121
|
+
const configured = Boolean(value?.trim());
|
|
122
|
+
if (!configured) {
|
|
123
|
+
return {
|
|
124
|
+
customEndpoint: false,
|
|
125
|
+
endpointValid: true,
|
|
126
|
+
endpointSecure: true
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const endpoint = new URL(value.trim());
|
|
131
|
+
const supportedProtocol = endpoint.protocol === "https:" || endpoint.protocol === "http:";
|
|
132
|
+
return {
|
|
133
|
+
customEndpoint: true,
|
|
134
|
+
endpointValid: supportedProtocol && !endpoint.username && !endpoint.password && !endpoint.hash && !endpoint.search,
|
|
135
|
+
endpointSecure: endpoint.protocol === "https:"
|
|
136
|
+
};
|
|
137
|
+
} catch {
|
|
138
|
+
return {
|
|
139
|
+
customEndpoint: true,
|
|
140
|
+
endpointValid: false,
|
|
141
|
+
endpointSecure: false
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
var createCredentials = (env, provider, names) => Object.freeze({
|
|
146
|
+
names,
|
|
147
|
+
get: (name) => {
|
|
148
|
+
if (!names.includes(name)) {
|
|
149
|
+
throw new Error(`Credential ${name} is not declared for provider ${provider}.`);
|
|
150
|
+
}
|
|
151
|
+
return env[name]?.trim() || undefined;
|
|
152
|
+
},
|
|
153
|
+
first: () => {
|
|
154
|
+
for (const name of names) {
|
|
155
|
+
const value = env[name]?.trim();
|
|
156
|
+
if (value) {
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
},
|
|
162
|
+
require: () => {
|
|
163
|
+
for (const name of names) {
|
|
164
|
+
const value = env[name]?.trim();
|
|
165
|
+
if (value) {
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
throw new Error(names.length === 0 ? `Provider ${provider} does not declare a credential.` : `Missing credentials for ${provider}. Set ${names.join(" or ")} in the environment.`);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
var normalizeRegistration = (registration) => {
|
|
173
|
+
if (typeof registration.factory !== "function") {
|
|
174
|
+
throw new Error("Provider registration factory must be a function.");
|
|
175
|
+
}
|
|
176
|
+
const descriptor = normalizedDescriptor(registration.descriptor);
|
|
177
|
+
return Object.freeze({
|
|
178
|
+
descriptor,
|
|
179
|
+
factory: registration.factory,
|
|
180
|
+
diagnostics: normalizedDiagnostics(descriptor.id, registration.diagnostics)
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
var providerConfiguration = (diagnostics, env) => {
|
|
184
|
+
const configuration = endpointConfiguration(diagnostics.endpointEnvironmentVariable ? env[diagnostics.endpointEnvironmentVariable] : undefined);
|
|
185
|
+
for (const entry of diagnostics.presence ?? []) {
|
|
186
|
+
configuration[`${entry.key}Configured`] = Boolean(env[entry.environmentVariable]?.trim());
|
|
187
|
+
}
|
|
188
|
+
for (const entry of diagnostics.enums ?? []) {
|
|
189
|
+
const value = env[entry.environmentVariable]?.trim();
|
|
190
|
+
configuration[`${entry.key}Configured`] = Boolean(value);
|
|
191
|
+
configuration[`${entry.key}Valid`] = !value || entry.allowedValues.includes(value);
|
|
192
|
+
}
|
|
193
|
+
return Object.freeze(configuration);
|
|
194
|
+
};
|
|
195
|
+
var parseWithRegistrations = (value, registrations) => {
|
|
196
|
+
const defaultProvider = registrations.has("openai") ? "openai" : registrations.keys().next().value;
|
|
197
|
+
const normalized = (value ?? defaultProvider ?? "").trim().toLowerCase();
|
|
198
|
+
if (registrations.has(normalized)) {
|
|
199
|
+
return normalized;
|
|
200
|
+
}
|
|
201
|
+
throw new Error(`Unknown provider: ${value}. Use ${[...registrations.keys()].join(", ")}.`);
|
|
202
|
+
};
|
|
203
|
+
var createProviderRegistry = (registrations) => {
|
|
204
|
+
if (registrations.length === 0) {
|
|
205
|
+
throw new Error("Provider registry must contain at least one provider.");
|
|
206
|
+
}
|
|
207
|
+
const entries = new Map;
|
|
208
|
+
for (const registration of registrations) {
|
|
209
|
+
const normalized = normalizeRegistration(registration);
|
|
210
|
+
if (entries.has(normalized.descriptor.id)) {
|
|
211
|
+
throw new Error(`Duplicate provider registration: ${normalized.descriptor.id}.`);
|
|
212
|
+
}
|
|
213
|
+
entries.set(normalized.descriptor.id, normalized);
|
|
214
|
+
}
|
|
215
|
+
const frozenRegistrations = Object.freeze([...entries.values()]);
|
|
216
|
+
const descriptors = Object.freeze(frozenRegistrations.map((entry) => entry.descriptor));
|
|
217
|
+
const registry = Object.freeze({
|
|
218
|
+
descriptors,
|
|
219
|
+
has: (provider) => entries.has(provider.trim().toLowerCase()),
|
|
220
|
+
parse: (value) => parseWithRegistrations(value, entries),
|
|
221
|
+
descriptor: (provider) => {
|
|
222
|
+
const registration = entries.get(provider);
|
|
223
|
+
if (!registration) {
|
|
224
|
+
throw new Error(`No descriptor is available for provider ${provider}.`);
|
|
225
|
+
}
|
|
226
|
+
return registration.descriptor;
|
|
227
|
+
},
|
|
228
|
+
createModel: (config, env = process.env) => {
|
|
229
|
+
const registration = entries.get(config.provider);
|
|
230
|
+
if (!registration) {
|
|
231
|
+
throw new Error(`No factory is available for provider ${config.provider}.`);
|
|
232
|
+
}
|
|
233
|
+
return registration.factory(Object.freeze({
|
|
234
|
+
provider: config.provider,
|
|
235
|
+
model: config.model,
|
|
236
|
+
env,
|
|
237
|
+
credentials: createCredentials(env, config.provider, registration.descriptor.credentialNames)
|
|
238
|
+
}));
|
|
239
|
+
},
|
|
240
|
+
availability: (env = process.env) => Object.freeze(frozenRegistrations.map((registration) => {
|
|
241
|
+
const credentials = Object.freeze(registration.descriptor.credentialNames.map((name) => Object.freeze({ name, present: Boolean(env[name]?.trim()) })));
|
|
242
|
+
return Object.freeze({
|
|
243
|
+
...registration.descriptor,
|
|
244
|
+
configured: credentials.length === 0 || credentials.some((credential) => credential.present),
|
|
245
|
+
credentials,
|
|
246
|
+
configuration: providerConfiguration(registration.diagnostics, env)
|
|
247
|
+
});
|
|
248
|
+
})),
|
|
249
|
+
transportFingerprint: (env = process.env) => {
|
|
250
|
+
const transport = frozenRegistrations.map((registration) => {
|
|
251
|
+
const names = [
|
|
252
|
+
registration.diagnostics.endpointEnvironmentVariable,
|
|
253
|
+
...(registration.diagnostics.presence ?? []).map((entry) => entry.environmentVariable),
|
|
254
|
+
...(registration.diagnostics.enums ?? []).map((entry) => entry.environmentVariable)
|
|
255
|
+
].filter((name) => Boolean(name));
|
|
256
|
+
return {
|
|
257
|
+
provider: registration.descriptor.id,
|
|
258
|
+
values: names.map((name) => [name, env[name]?.trim() ?? ""])
|
|
259
|
+
};
|
|
260
|
+
});
|
|
261
|
+
return `sha256:${createHash("sha256").update(JSON.stringify(transport)).digest("hex")}`;
|
|
262
|
+
},
|
|
263
|
+
extend: (additional) => createProviderRegistry([
|
|
264
|
+
...frozenRegistrations,
|
|
265
|
+
...additional
|
|
266
|
+
])
|
|
267
|
+
});
|
|
268
|
+
return registry;
|
|
269
|
+
};
|
|
270
|
+
var optionalQwenRegion = (value) => {
|
|
271
|
+
if (!value) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (QWEN_REGIONS.includes(value)) {
|
|
275
|
+
return value;
|
|
276
|
+
}
|
|
277
|
+
throw new Error(`Invalid QWEN_REGION: ${value}.`);
|
|
278
|
+
};
|
|
279
|
+
var BUILTIN_PROVIDER_REGISTRATIONS = Object.freeze([
|
|
280
|
+
{
|
|
281
|
+
descriptor: {
|
|
282
|
+
id: "meta",
|
|
283
|
+
name: "Meta Model API",
|
|
284
|
+
defaultModel: "muse-spark-1.2",
|
|
285
|
+
credentialNames: ["MODEL_API_KEY"],
|
|
286
|
+
capabilities: HARNESS_PROVIDER_CAPABILITIES,
|
|
287
|
+
support: "certified"
|
|
288
|
+
},
|
|
289
|
+
diagnostics: { endpointEnvironmentVariable: "META_BASE_URL" },
|
|
290
|
+
factory: ({ model, env, credentials }) => {
|
|
291
|
+
const baseURL = env.META_BASE_URL?.trim();
|
|
292
|
+
return createMeta({
|
|
293
|
+
apiKey: credentials.require(),
|
|
294
|
+
...baseURL ? { baseURL } : {}
|
|
295
|
+
})(model);
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
descriptor: {
|
|
300
|
+
id: "qwen",
|
|
301
|
+
name: "Qwen / Alibaba Cloud Model Studio",
|
|
302
|
+
defaultModel: "qwen3.8-max",
|
|
303
|
+
credentialNames: ["DASHSCOPE_API_KEY", "QWEN_API_KEY"],
|
|
304
|
+
capabilities: HARNESS_PROVIDER_CAPABILITIES,
|
|
305
|
+
support: "certified"
|
|
306
|
+
},
|
|
307
|
+
diagnostics: {
|
|
308
|
+
endpointEnvironmentVariable: "QWEN_BASE_URL",
|
|
309
|
+
presence: [{ key: "workspaceId", environmentVariable: "QWEN_WORKSPACE_ID" }],
|
|
310
|
+
enums: [{
|
|
311
|
+
key: "region",
|
|
312
|
+
environmentVariable: "QWEN_REGION",
|
|
313
|
+
allowedValues: QWEN_REGIONS
|
|
314
|
+
}]
|
|
315
|
+
},
|
|
316
|
+
factory: ({ model, env, credentials }) => {
|
|
317
|
+
const baseURL = env.QWEN_BASE_URL?.trim();
|
|
318
|
+
const workspaceId = env.QWEN_WORKSPACE_ID?.trim();
|
|
319
|
+
const region = optionalQwenRegion(env.QWEN_REGION?.trim());
|
|
320
|
+
return createQwen({
|
|
321
|
+
apiKey: credentials.require(),
|
|
322
|
+
...baseURL ? { baseURL } : {},
|
|
323
|
+
...workspaceId ? { workspaceId } : {},
|
|
324
|
+
...region ? { region } : {}
|
|
325
|
+
})(model);
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
descriptor: {
|
|
330
|
+
id: "openai",
|
|
331
|
+
name: "OpenAI",
|
|
332
|
+
defaultModel: "gpt-5.6-luna",
|
|
333
|
+
credentialNames: ["OPENAI_API_KEY"],
|
|
334
|
+
capabilities: HARNESS_PROVIDER_CAPABILITIES,
|
|
335
|
+
support: "certified"
|
|
336
|
+
},
|
|
337
|
+
diagnostics: { endpointEnvironmentVariable: "OPENAI_BASE_URL" },
|
|
338
|
+
factory: ({ model, env, credentials }) => {
|
|
339
|
+
const baseURL = env.OPENAI_BASE_URL?.trim();
|
|
340
|
+
return createOpenAI({
|
|
341
|
+
apiKey: credentials.require(),
|
|
342
|
+
...baseURL ? { baseURL } : {}
|
|
343
|
+
})(model);
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
descriptor: {
|
|
348
|
+
id: "gemini",
|
|
349
|
+
name: "Google Gemini",
|
|
350
|
+
defaultModel: "gemini-3.6-flash",
|
|
351
|
+
credentialNames: ["GEMINI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"],
|
|
352
|
+
capabilities: HARNESS_PROVIDER_CAPABILITIES,
|
|
353
|
+
support: "provisional"
|
|
354
|
+
},
|
|
355
|
+
diagnostics: { endpointEnvironmentVariable: "GEMINI_BASE_URL" },
|
|
356
|
+
factory: ({ model, env, credentials }) => {
|
|
357
|
+
const baseURL = env.GEMINI_BASE_URL?.trim();
|
|
358
|
+
return createGemini({
|
|
359
|
+
apiKey: credentials.require(),
|
|
360
|
+
...baseURL ? { baseURL } : {}
|
|
361
|
+
})(model);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
]);
|
|
365
|
+
var DEFAULT_PROVIDER_REGISTRY = createProviderRegistry(BUILTIN_PROVIDER_REGISTRATIONS);
|
|
366
|
+
var PROVIDER_DESCRIPTORS = DEFAULT_PROVIDER_REGISTRY.descriptors;
|
|
367
|
+
|
|
368
|
+
// src/config.ts
|
|
21
369
|
var DEFAULT_ALLOWED_CHECKS = ["test", "typecheck", "lint", "build"];
|
|
22
370
|
var HARNESS_CONFIG_SCHEMA_VERSION = 4;
|
|
23
371
|
var HARNESS_STORE_BACKENDS = ["sqlite", "file"];
|
|
@@ -73,45 +421,6 @@ var DEFAULT_SUBAGENT_BUDGET = {
|
|
|
73
421
|
maxTotalTokens: 36000,
|
|
74
422
|
includeChildRuns: false
|
|
75
423
|
};
|
|
76
|
-
var HARNESS_PROVIDER_CAPABILITIES = [
|
|
77
|
-
"streaming",
|
|
78
|
-
"tool-calling",
|
|
79
|
-
"approval-resume"
|
|
80
|
-
];
|
|
81
|
-
var PROVIDER_DESCRIPTORS = [
|
|
82
|
-
{
|
|
83
|
-
id: "meta",
|
|
84
|
-
name: "Meta Model API",
|
|
85
|
-
defaultModel: "muse-spark-1.2",
|
|
86
|
-
credentialNames: ["MODEL_API_KEY"],
|
|
87
|
-
capabilities: HARNESS_PROVIDER_CAPABILITIES,
|
|
88
|
-
support: "certified"
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
id: "qwen",
|
|
92
|
-
name: "Qwen / Alibaba Cloud Model Studio",
|
|
93
|
-
defaultModel: "qwen3.8-max",
|
|
94
|
-
credentialNames: ["DASHSCOPE_API_KEY", "QWEN_API_KEY"],
|
|
95
|
-
capabilities: HARNESS_PROVIDER_CAPABILITIES,
|
|
96
|
-
support: "certified"
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
id: "openai",
|
|
100
|
-
name: "OpenAI",
|
|
101
|
-
defaultModel: "gpt-5.4",
|
|
102
|
-
credentialNames: ["OPENAI_API_KEY"],
|
|
103
|
-
capabilities: HARNESS_PROVIDER_CAPABILITIES,
|
|
104
|
-
support: "certified"
|
|
105
|
-
}
|
|
106
|
-
];
|
|
107
|
-
var QWEN_REGIONS = [
|
|
108
|
-
"singapore",
|
|
109
|
-
"beijing",
|
|
110
|
-
"hong-kong",
|
|
111
|
-
"tokyo",
|
|
112
|
-
"frankfurt",
|
|
113
|
-
"virginia"
|
|
114
|
-
];
|
|
115
424
|
var integerOption = (name, input, envValue, fallback, minimum, maximum) => {
|
|
116
425
|
const value = input ?? (envValue === undefined ? fallback : Number(envValue));
|
|
117
426
|
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
@@ -140,7 +449,7 @@ var nonNegativeNumberOption = (name, input, envValue) => {
|
|
|
140
449
|
}
|
|
141
450
|
return value;
|
|
142
451
|
};
|
|
143
|
-
var defaultHarnessNamespace = (workspace) => `workspace-${
|
|
452
|
+
var defaultHarnessNamespace = (workspace) => `workspace-${createHash2("sha256").update(workspace).digest("hex").slice(0, 16)}`;
|
|
144
453
|
var canonicalStateDirectory = (requestedWorkspace, canonicalWorkspace, configured) => {
|
|
145
454
|
if (!configured) {
|
|
146
455
|
return path.join(canonicalWorkspace, ".zhivex-harness", "runs");
|
|
@@ -242,26 +551,14 @@ var canonicalWorkspaceFile = (requestedWorkspace, canonicalWorkspace, configured
|
|
|
242
551
|
const insideRequestedWorkspace = relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
243
552
|
return insideRequestedWorkspace ? path.resolve(canonicalWorkspace, relative) : requestedPath;
|
|
244
553
|
};
|
|
245
|
-
var parseProvider = (value) =>
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
return normalized;
|
|
249
|
-
}
|
|
250
|
-
throw new Error(`Unknown provider: ${value}. Use meta, qwen, or openai.`);
|
|
251
|
-
};
|
|
252
|
-
var providerDescriptor = (provider) => {
|
|
253
|
-
const descriptor = PROVIDER_DESCRIPTORS.find((entry) => entry.id === provider);
|
|
254
|
-
if (!descriptor) {
|
|
255
|
-
throw new Error(`No descriptor is available for provider ${provider}.`);
|
|
256
|
-
}
|
|
257
|
-
return descriptor;
|
|
258
|
-
};
|
|
259
|
-
var resolveHarnessConfig = (input = {}) => {
|
|
554
|
+
var parseProvider = (value, registry = DEFAULT_PROVIDER_REGISTRY) => registry.parse(value);
|
|
555
|
+
var providerDescriptor = (provider, registry = DEFAULT_PROVIDER_REGISTRY) => registry.descriptor(provider);
|
|
556
|
+
var resolveHarnessConfig = (input = {}, registry = DEFAULT_PROVIDER_REGISTRY) => {
|
|
260
557
|
if (input.schemaVersion !== undefined && input.schemaVersion !== HARNESS_CONFIG_SCHEMA_VERSION) {
|
|
261
558
|
throw new Error(`Unsupported config schema version: ${input.schemaVersion}. Expected ${HARNESS_CONFIG_SCHEMA_VERSION}.`);
|
|
262
559
|
}
|
|
263
|
-
const provider = parseProvider(input.provider ?? process.env.ZHIVEX_HARNESS_PROVIDER);
|
|
264
|
-
const descriptor = providerDescriptor(provider);
|
|
560
|
+
const provider = parseProvider(input.provider ?? process.env.ZHIVEX_HARNESS_PROVIDER, registry);
|
|
561
|
+
const descriptor = providerDescriptor(provider, registry);
|
|
265
562
|
const requestedWorkspace = path.resolve(input.workspace ?? process.cwd());
|
|
266
563
|
let workspace = requestedWorkspace;
|
|
267
564
|
try {
|
|
@@ -355,105 +652,11 @@ var resolveHarnessConfig = (input = {}) => {
|
|
|
355
652
|
...mcpConfigPath ? { mcpConfigPath } : {}
|
|
356
653
|
};
|
|
357
654
|
};
|
|
358
|
-
var
|
|
359
|
-
|
|
360
|
-
const value = env[name]?.trim();
|
|
361
|
-
if (value) {
|
|
362
|
-
return value;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
throw new Error(`Missing credentials for ${provider}. Set ${names.join(" or ")} in the environment.`);
|
|
366
|
-
};
|
|
367
|
-
var optionalQwenRegion = (value) => {
|
|
368
|
-
if (!value) {
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
if (QWEN_REGIONS.includes(value)) {
|
|
372
|
-
return value;
|
|
373
|
-
}
|
|
374
|
-
throw new Error(`Invalid QWEN_REGION: ${value}.`);
|
|
375
|
-
};
|
|
376
|
-
var createProviderModel = (config, env = process.env) => {
|
|
377
|
-
switch (config.provider) {
|
|
378
|
-
case "meta": {
|
|
379
|
-
const baseURL = env.META_BASE_URL?.trim();
|
|
380
|
-
const provider = createMeta({
|
|
381
|
-
apiKey: requiredCredential(env, "meta", ["MODEL_API_KEY"]),
|
|
382
|
-
...baseURL ? { baseURL } : {}
|
|
383
|
-
});
|
|
384
|
-
return provider(config.model);
|
|
385
|
-
}
|
|
386
|
-
case "qwen": {
|
|
387
|
-
const baseURL = env.QWEN_BASE_URL?.trim();
|
|
388
|
-
const workspaceId = env.QWEN_WORKSPACE_ID?.trim();
|
|
389
|
-
const region = optionalQwenRegion(env.QWEN_REGION?.trim());
|
|
390
|
-
const provider = createQwen({
|
|
391
|
-
apiKey: requiredCredential(env, "qwen", ["DASHSCOPE_API_KEY", "QWEN_API_KEY"]),
|
|
392
|
-
...baseURL ? { baseURL } : {},
|
|
393
|
-
...workspaceId ? { workspaceId } : {},
|
|
394
|
-
...region ? { region } : {}
|
|
395
|
-
});
|
|
396
|
-
return provider(config.model);
|
|
397
|
-
}
|
|
398
|
-
case "openai": {
|
|
399
|
-
const baseURL = env.OPENAI_BASE_URL?.trim();
|
|
400
|
-
const provider = createOpenAI({
|
|
401
|
-
apiKey: requiredCredential(env, "openai", ["OPENAI_API_KEY"]),
|
|
402
|
-
...baseURL ? { baseURL } : {}
|
|
403
|
-
});
|
|
404
|
-
return provider(config.model);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
};
|
|
408
|
-
var endpointConfiguration = (value) => {
|
|
409
|
-
const configured = Boolean(value?.trim());
|
|
410
|
-
if (!configured) {
|
|
411
|
-
return {
|
|
412
|
-
customEndpoint: false,
|
|
413
|
-
endpointValid: true,
|
|
414
|
-
endpointSecure: true
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
try {
|
|
418
|
-
const endpoint = new URL(value.trim());
|
|
419
|
-
const supportedProtocol = endpoint.protocol === "https:" || endpoint.protocol === "http:";
|
|
420
|
-
return {
|
|
421
|
-
customEndpoint: true,
|
|
422
|
-
endpointValid: supportedProtocol && !endpoint.username && !endpoint.password && !endpoint.hash,
|
|
423
|
-
endpointSecure: endpoint.protocol === "https:"
|
|
424
|
-
};
|
|
425
|
-
} catch {
|
|
426
|
-
return {
|
|
427
|
-
customEndpoint: true,
|
|
428
|
-
endpointValid: false,
|
|
429
|
-
endpointSecure: false
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
};
|
|
433
|
-
var providerAvailability = (env = process.env) => PROVIDER_DESCRIPTORS.map((descriptor) => {
|
|
434
|
-
const credentials = descriptor.credentialNames.map((name) => ({
|
|
435
|
-
name,
|
|
436
|
-
present: Boolean(env[name]?.trim())
|
|
437
|
-
}));
|
|
438
|
-
const endpointVariable = descriptor.id === "meta" ? "META_BASE_URL" : descriptor.id === "qwen" ? "QWEN_BASE_URL" : "OPENAI_BASE_URL";
|
|
439
|
-
const regionValue = descriptor.id === "qwen" ? env.QWEN_REGION?.trim() : undefined;
|
|
440
|
-
return {
|
|
441
|
-
...descriptor,
|
|
442
|
-
configured: credentials.some((credential) => credential.present),
|
|
443
|
-
credentials,
|
|
444
|
-
configuration: {
|
|
445
|
-
...endpointConfiguration(env[endpointVariable]),
|
|
446
|
-
...descriptor.id === "qwen" ? {
|
|
447
|
-
workspaceIdConfigured: Boolean(env.QWEN_WORKSPACE_ID?.trim()),
|
|
448
|
-
regionConfigured: Boolean(regionValue),
|
|
449
|
-
regionValid: !regionValue || QWEN_REGIONS.includes(regionValue)
|
|
450
|
-
} : {}
|
|
451
|
-
}
|
|
452
|
-
};
|
|
453
|
-
});
|
|
655
|
+
var createProviderModel = (config, env = process.env, registry = DEFAULT_PROVIDER_REGISTRY) => registry.createModel(config, env);
|
|
656
|
+
var providerAvailability = (env = process.env, registry = DEFAULT_PROVIDER_REGISTRY) => registry.availability(env);
|
|
454
657
|
|
|
455
658
|
// src/harness.ts
|
|
456
|
-
import { createHash as
|
|
659
|
+
import { createHash as createHash7 } from "crypto";
|
|
457
660
|
import {
|
|
458
661
|
Agent,
|
|
459
662
|
applySafetyPolicyToAgent as applySafetyPolicyToAgent2,
|
|
@@ -523,7 +726,7 @@ var selectHarnessModel = (candidates, required) => {
|
|
|
523
726
|
};
|
|
524
727
|
|
|
525
728
|
// src/edit-contracts.ts
|
|
526
|
-
import { createHash as
|
|
729
|
+
import { createHash as createHash3 } from "crypto";
|
|
527
730
|
import path2 from "path";
|
|
528
731
|
import { z } from "zod";
|
|
529
732
|
var EDIT_CONTRACT_SCHEMA_VERSION = 1;
|
|
@@ -623,7 +826,7 @@ var mutationAuditEntrySchema = z.strictObject({
|
|
|
623
826
|
timestamp: z.iso.datetime(),
|
|
624
827
|
quarantineId: z.string().min(1).max(200).optional()
|
|
625
828
|
});
|
|
626
|
-
var digest = (value) => `sha256:${
|
|
829
|
+
var digest = (value) => `sha256:${createHash3("sha256").update(value).digest("hex")}`;
|
|
627
830
|
var canonicalProposalPayload = (changes) => ({
|
|
628
831
|
schemaVersion: EDIT_CONTRACT_SCHEMA_VERSION,
|
|
629
832
|
kind: "edit-proposal",
|
|
@@ -658,7 +861,7 @@ var editContractDocument = (kind, result) => ({
|
|
|
658
861
|
});
|
|
659
862
|
|
|
660
863
|
// src/workspace.ts
|
|
661
|
-
import { createHash as
|
|
864
|
+
import { createHash as createHash4, randomUUID } from "crypto";
|
|
662
865
|
import { link, lstat, mkdir, open, readFile, readdir, realpath, rename, stat, unlink } from "fs/promises";
|
|
663
866
|
import path3 from "path";
|
|
664
867
|
var HARD_IGNORES = new Set([".git", ".next", ".turbo", ".zhivex-harness", "coverage", "dist", "node_modules"]);
|
|
@@ -676,7 +879,7 @@ var isInside = (root, candidate) => {
|
|
|
676
879
|
return relative === "" || !relative.startsWith(`..${path3.sep}`) && relative !== ".." && !path3.isAbsolute(relative);
|
|
677
880
|
};
|
|
678
881
|
var wirePath = (value) => value.split(path3.sep).join("/");
|
|
679
|
-
var digestBytes = (value) => `sha256:${
|
|
882
|
+
var digestBytes = (value) => `sha256:${createHash4("sha256").update(value).digest("hex")}`;
|
|
680
883
|
var truncate = (value, maxLength = MAX_TOOL_OUTPUT) => value.length <= maxLength ? value : `${value.slice(0, maxLength)}
|
|
681
884
|
\u2026 output truncated (${value.length - maxLength} characters omitted)`;
|
|
682
885
|
var cursorEncode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
@@ -2173,7 +2376,7 @@ var createHarnessMcpTools = async (configuration, options = {}) => {
|
|
|
2173
2376
|
var mcpConfigurationFingerprintInput = (configuration) => serializeJsonValue(configuration);
|
|
2174
2377
|
|
|
2175
2378
|
// src/orchestration.ts
|
|
2176
|
-
import { createHash as
|
|
2379
|
+
import { createHash as createHash5, randomUUID as randomUUID2 } from "crypto";
|
|
2177
2380
|
import {
|
|
2178
2381
|
applySafetyPolicyToAgent,
|
|
2179
2382
|
createBudgetGuard,
|
|
@@ -2220,7 +2423,7 @@ var childHarnessBinding = (parent, profile, model, toolNames, config) => ({
|
|
|
2220
2423
|
schemaVersion: 1,
|
|
2221
2424
|
id: `${parent.id}-${profile}`,
|
|
2222
2425
|
version: parent.version,
|
|
2223
|
-
fingerprint: `sha256:${
|
|
2426
|
+
fingerprint: `sha256:${createHash5("sha256").update(JSON.stringify({
|
|
2224
2427
|
parent: parent.fingerprint,
|
|
2225
2428
|
profile,
|
|
2226
2429
|
model: {
|
|
@@ -2344,7 +2547,7 @@ var runHarnessReviewGroup = async (runtime, input, profiles = ["explorer", "revi
|
|
|
2344
2547
|
};
|
|
2345
2548
|
|
|
2346
2549
|
// src/execution-environment.ts
|
|
2347
|
-
import { createHash as
|
|
2550
|
+
import { createHash as createHash6, randomUUID as randomUUID3 } from "crypto";
|
|
2348
2551
|
import {
|
|
2349
2552
|
chmod as chmod2,
|
|
2350
2553
|
lstat as lstat5,
|
|
@@ -2384,8 +2587,8 @@ var BUILT_IN_TOOL_NAMES = new Set([
|
|
|
2384
2587
|
"apply_environment_patch",
|
|
2385
2588
|
"environment_status"
|
|
2386
2589
|
]);
|
|
2387
|
-
var digest2 = (value) => `sha256:${
|
|
2388
|
-
var runHash = (runId) =>
|
|
2590
|
+
var digest2 = (value) => `sha256:${createHash6("sha256").update(value).digest("hex")}`;
|
|
2591
|
+
var runHash = (runId) => createHash6("sha256").update(runId).digest("hex").slice(0, 24);
|
|
2389
2592
|
var isInside3 = (root, candidate) => {
|
|
2390
2593
|
const relative = path7.relative(root, candidate);
|
|
2391
2594
|
return relative === "" || relative !== ".." && !relative.startsWith(`..${path7.sep}`) && !path7.isAbsolute(relative);
|
|
@@ -3357,11 +3560,11 @@ var describeOciCommand = (command, args) => commandDisplay([command, ...args]);
|
|
|
3357
3560
|
// src/harness.ts
|
|
3358
3561
|
var APPROVAL_VERSION = "2026-08-17-v5";
|
|
3359
3562
|
var TOOL_CONTRACT_VERSION = "enforced-execution-v1";
|
|
3360
|
-
var createHarnessBinding = (config, mcpConfiguration, model, subagentModels, executionEnvironment) => ({
|
|
3563
|
+
var createHarnessBinding = (config, mcpConfiguration, model, subagentModels, providerTransportFingerprint, executionEnvironment) => ({
|
|
3361
3564
|
schemaVersion: 1,
|
|
3362
3565
|
id: "zhivex-harness",
|
|
3363
3566
|
version: HARNESS_VERSION,
|
|
3364
|
-
fingerprint: `sha256:${
|
|
3567
|
+
fingerprint: `sha256:${createHash7("sha256").update(JSON.stringify({
|
|
3365
3568
|
configSchemaVersion: HARNESS_CONFIG_SCHEMA_VERSION,
|
|
3366
3569
|
approvalVersion: APPROVAL_VERSION,
|
|
3367
3570
|
toolContractVersion: TOOL_CONTRACT_VERSION,
|
|
@@ -3369,6 +3572,7 @@ var createHarnessBinding = (config, mcpConfiguration, model, subagentModels, exe
|
|
|
3369
3572
|
provider: config.provider,
|
|
3370
3573
|
model: config.model,
|
|
3371
3574
|
runtimeModel: inspectHarnessModelCapabilities(model),
|
|
3575
|
+
providerTransportFingerprint,
|
|
3372
3576
|
subagentModels: Object.fromEntries(config.orchestration.profiles.map((profile) => [
|
|
3373
3577
|
profile,
|
|
3374
3578
|
inspectHarnessModelCapabilities(subagentModels?.[profile] ?? model)
|
|
@@ -3390,6 +3594,7 @@ Rules:
|
|
|
3390
3594
|
- Make the smallest coherent change that fully addresses the task.
|
|
3391
3595
|
- For every file edit, first read its digest and call propose_edits. Apply exactly that reviewed proposal with apply_patch.
|
|
3392
3596
|
- apply_patch, move_file, quarantine_file, restore_file, and run_check require explicit approval from the operator.
|
|
3597
|
+
- Calling an approval-gated tool is how you request that approval: call the tool with its complete reviewed arguments, then let the runtime pause before execution. Never replace the tool call with a textual approval request.
|
|
3393
3598
|
- With enforced OCI execution enabled, workspace tools operate on an ephemeral snapshot. Use inspect_environment_patch and obtain a separate approval through apply_environment_patch before changing the host workspace.
|
|
3394
3599
|
- run_environment_command executes one allowlisted argv command without a host shell. Network, privileges, resources, environment variables, and output are bounded by the OCI policy.
|
|
3395
3600
|
- Never overwrite stale content. If an expected digest no longer matches, inspect the file again and create a new proposal.
|
|
@@ -3485,7 +3690,7 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
|
|
|
3485
3690
|
}),
|
|
3486
3691
|
apply_patch: tool({
|
|
3487
3692
|
name: "apply_patch",
|
|
3488
|
-
description: "
|
|
3693
|
+
description: "Request approval to atomically apply one reviewed multi-file proposal; the runtime pauses before execution. Every existing file requires its exact expected digest; expectedDigest=null is create-only. Call this tool instead of asking for approval in text.",
|
|
3489
3694
|
schema: applyEditProposalInputSchema,
|
|
3490
3695
|
...mutationApproval,
|
|
3491
3696
|
execute: async (input, context) => {
|
|
@@ -3591,32 +3796,49 @@ var createExecutionEnvironmentTools = (workspace) => ({
|
|
|
3591
3796
|
})
|
|
3592
3797
|
});
|
|
3593
3798
|
var estimateMessageTokens = (messages) => Math.max(1, Math.ceil(JSON.stringify(messages).length / 4));
|
|
3594
|
-
var
|
|
3799
|
+
var summarizeHarnessMessages = (messages) => {
|
|
3595
3800
|
const redaction = createRedactionPolicy2({ includeEmails: true });
|
|
3596
|
-
|
|
3597
|
-
const
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
return typeof part.type === "string" ? part.type : "part";
|
|
3613
|
-
});
|
|
3614
|
-
return `${index + 1}. ${record.role ?? "message"}: ${parts.join(" | ")}`;
|
|
3801
|
+
const summaries = messages.map((message, index) => {
|
|
3802
|
+
const record = message;
|
|
3803
|
+
const parts = (record.parts ?? []).map((part) => {
|
|
3804
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
3805
|
+
const text = redaction.redactText(part.text).replace(/\bBearer\s+\S+/gi, "Bearer [REDACTED]").replace(/\b([A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD)|api[_-]?key|access[_-]?token|password)\s*([=:])\s*(?:"[^"]*"|'[^']*'|\S+)/gi, "$1$2[REDACTED]").replace(/\b(?:sk|ghp|gho|github_pat)-[A-Za-z0-9_-]{8,}\b/g, "[REDACTED]").replace(/\s+/g, " ").trim();
|
|
3806
|
+
return text.length > 160 ? `${text.slice(0, 160)}\u2026` : text;
|
|
3807
|
+
}
|
|
3808
|
+
if (part.type === "tool-call") {
|
|
3809
|
+
const call = part.toolCall;
|
|
3810
|
+
return `tool-call:${typeof call?.name === "string" ? call.name : "unknown"}`;
|
|
3811
|
+
}
|
|
3812
|
+
if (part.type === "tool-result") {
|
|
3813
|
+
const result = part.toolResult;
|
|
3814
|
+
return `tool-result:${typeof result?.toolName === "string" ? result.toolName : "unknown"}`;
|
|
3815
|
+
}
|
|
3816
|
+
return typeof part.type === "string" ? part.type : "part";
|
|
3615
3817
|
});
|
|
3616
|
-
|
|
3818
|
+
return `${index + 1}. ${record.role ?? "message"}: ${parts.join(" | ")}`;
|
|
3819
|
+
});
|
|
3820
|
+
const summary = summaries.join(`
|
|
3617
3821
|
`);
|
|
3822
|
+
return summary.length > 4000 ? `${summary.slice(0, 4000)}\u2026` : summary;
|
|
3823
|
+
};
|
|
3824
|
+
var compactHarnessMessages = (messages) => {
|
|
3825
|
+
if (messages.length === 0)
|
|
3826
|
+
return [];
|
|
3827
|
+
return [
|
|
3828
|
+
{
|
|
3829
|
+
role: "user",
|
|
3830
|
+
parts: [{
|
|
3831
|
+
type: "text",
|
|
3832
|
+
text: `[Compacted conversation context]
|
|
3833
|
+
${summarizeHarnessMessages(messages)}`
|
|
3834
|
+
}]
|
|
3835
|
+
}
|
|
3836
|
+
];
|
|
3837
|
+
};
|
|
3838
|
+
var createHarnessCompactor = () => {
|
|
3839
|
+
return async ({ messages }) => {
|
|
3618
3840
|
return {
|
|
3619
|
-
summary:
|
|
3841
|
+
summary: summarizeHarnessMessages(messages),
|
|
3620
3842
|
metadata: {
|
|
3621
3843
|
strategy: "deterministic-redacted-transcript",
|
|
3622
3844
|
sourceMessages: messages.length
|
|
@@ -3679,7 +3901,7 @@ var createProviderCompatibleBudget = (config) => {
|
|
|
3679
3901
|
};
|
|
3680
3902
|
};
|
|
3681
3903
|
var createHarness = async (options = {}) => {
|
|
3682
|
-
const config = resolveHarnessConfig(options);
|
|
3904
|
+
const config = resolveHarnessConfig(options, options.providerRegistry);
|
|
3683
3905
|
const workspace = await Workspace.open(config.workspace);
|
|
3684
3906
|
await validateStateDirectory(config.workspace, config.stateDirectory);
|
|
3685
3907
|
const executionEnvironment = config.execution.backend === "oci" ? await createHarnessOciExecutionEnvironment({
|
|
@@ -3688,7 +3910,7 @@ var createHarness = async (options = {}) => {
|
|
|
3688
3910
|
stateDirectory: config.stateDirectory,
|
|
3689
3911
|
...options.ociRuntimeAdapter ? { runtime: options.ociRuntimeAdapter } : {}
|
|
3690
3912
|
}) : undefined;
|
|
3691
|
-
const model = options.modelInstance ?? createProviderModel(config, options.env ?? process.env);
|
|
3913
|
+
const model = options.modelInstance ?? createProviderModel(config, options.env ?? process.env, options.providerRegistry);
|
|
3692
3914
|
const capabilityRequirements = [...new Set([
|
|
3693
3915
|
...config.requiredCapabilities,
|
|
3694
3916
|
...config.orchestration.profiles.length > 0 || config.mcpConfigPath || options.mcpConfiguration ? ["tools", "streaming"] : []
|
|
@@ -3730,7 +3952,7 @@ var createHarness = async (options = {}) => {
|
|
|
3730
3952
|
await options.onTelemetryEvent?.(event);
|
|
3731
3953
|
};
|
|
3732
3954
|
const costGuardrails = createCostGuardrails(config);
|
|
3733
|
-
const binding = createHarnessBinding(config, mcpConfiguration, model, options.subagentModels, executionEnvironment);
|
|
3955
|
+
const binding = createHarnessBinding(config, mcpConfiguration, model, options.subagentModels, (options.providerRegistry ?? DEFAULT_PROVIDER_REGISTRY).transportFingerprint(options.env ?? process.env), executionEnvironment);
|
|
3734
3956
|
const subagentRuntime = createHarnessSubagents({
|
|
3735
3957
|
config,
|
|
3736
3958
|
parentBinding: binding,
|
|
@@ -3862,33 +4084,851 @@ var appendUserMessage = (messages, text) => [
|
|
|
3862
4084
|
import {
|
|
3863
4085
|
estimateAgentRunCost
|
|
3864
4086
|
} from "@zhivex-ai/agents/ops";
|
|
3865
|
-
var CLI_JSON_SCHEMA_VERSION = 1;
|
|
3866
|
-
var HARNESS_RESUME_METADATA_KEY = "zhivexHarnessResume";
|
|
3867
|
-
var HARNESS_RESUME_METADATA_SCHEMA_VERSION = 1;
|
|
3868
|
-
var CLI_EXIT_CODES = {
|
|
3869
|
-
success: 0,
|
|
3870
|
-
runtimeError: 1,
|
|
3871
|
-
usageError: 2,
|
|
3872
|
-
doctorFailed: 3
|
|
3873
|
-
};
|
|
3874
|
-
var COMMANDS = new Set(["run", "review", "chat", "providers", "doctor", "resume", "runs", "help", "version"]);
|
|
3875
|
-
var RUNS_COMMANDS = new Set(["list", "inspect", "cancel", "cleanup", "export"]);
|
|
3876
|
-
var RUN_STATUSES = new Set([
|
|
3877
|
-
"queued",
|
|
3878
|
-
"running",
|
|
3879
|
-
"completed",
|
|
3880
|
-
"suspended",
|
|
3881
|
-
"waiting_approval",
|
|
3882
|
-
"cancel_requested",
|
|
3883
|
-
"failed",
|
|
3884
|
-
"cancelled",
|
|
3885
|
-
"timed_out"
|
|
3886
|
-
]);
|
|
3887
4087
|
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
4088
|
+
// src/routing.ts
|
|
4089
|
+
var routeError = (value) => new Error(`Invalid route: ${value}. Use <profile>=<provider>[:<model>].`);
|
|
4090
|
+
var parseHarnessModelRoute = (value, registry = DEFAULT_PROVIDER_REGISTRY) => {
|
|
4091
|
+
const separator = value.indexOf("=");
|
|
4092
|
+
if (separator <= 0 || separator === value.length - 1) {
|
|
4093
|
+
throw routeError(value);
|
|
4094
|
+
}
|
|
4095
|
+
const profile = value.slice(0, separator).trim().toLowerCase();
|
|
4096
|
+
if (!HARNESS_SUBAGENT_PROFILES.includes(profile)) {
|
|
4097
|
+
throw routeError(value);
|
|
4098
|
+
}
|
|
4099
|
+
const target = value.slice(separator + 1).trim();
|
|
4100
|
+
const modelSeparator = target.indexOf(":");
|
|
4101
|
+
const providerInput = (modelSeparator === -1 ? target : target.slice(0, modelSeparator)).trim();
|
|
4102
|
+
const provider = registry.parse(providerInput);
|
|
4103
|
+
const configuredModel = modelSeparator === -1 ? "" : target.slice(modelSeparator + 1).trim();
|
|
4104
|
+
if (modelSeparator !== -1 && !configuredModel) {
|
|
4105
|
+
throw routeError(value);
|
|
4106
|
+
}
|
|
4107
|
+
const model = configuredModel || registry.descriptor(provider).defaultModel;
|
|
4108
|
+
if (/[\u0000-\u001f\u007f]/.test(model) || model.length > 256) {
|
|
4109
|
+
throw routeError(value);
|
|
4110
|
+
}
|
|
4111
|
+
return Object.freeze({
|
|
4112
|
+
profile,
|
|
4113
|
+
provider,
|
|
4114
|
+
model
|
|
4115
|
+
});
|
|
4116
|
+
};
|
|
4117
|
+
var resolveHarnessModelRoutes = (values = [], registry = DEFAULT_PROVIDER_REGISTRY) => {
|
|
4118
|
+
const routes = new Map;
|
|
4119
|
+
for (const value of values) {
|
|
4120
|
+
const route = parseHarnessModelRoute(value, registry);
|
|
4121
|
+
if (routes.has(route.profile)) {
|
|
4122
|
+
throw new Error(`Duplicate model route for ${route.profile}.`);
|
|
4123
|
+
}
|
|
4124
|
+
routes.set(route.profile, route);
|
|
4125
|
+
}
|
|
4126
|
+
return routes;
|
|
4127
|
+
};
|
|
4128
|
+
var createHarnessRouteModels = (routes, env = process.env, registry = DEFAULT_PROVIDER_REGISTRY) => Object.fromEntries([...routes].map(([profile, route]) => [
|
|
4129
|
+
profile,
|
|
4130
|
+
registry.createModel(route, env)
|
|
4131
|
+
]));
|
|
4132
|
+
var serializeHarnessModelRoutes = (routes) => Object.fromEntries([...routes].map(([profile, route]) => [
|
|
4133
|
+
profile,
|
|
4134
|
+
{ provider: route.provider, model: route.model }
|
|
4135
|
+
]));
|
|
4136
|
+
|
|
4137
|
+
// src/cli-stream.ts
|
|
4138
|
+
var CLI_JSON_SCHEMA_VERSION = 1;
|
|
4139
|
+
var CLI_EVENT_SCHEMA_VERSION = 1;
|
|
4140
|
+
var streamEventDocument = (event, sequence = 0) => {
|
|
4141
|
+
if (!Number.isSafeInteger(sequence) || sequence < 0) {
|
|
4142
|
+
throw new Error("JSONL event sequence must be a non-negative integer.");
|
|
4143
|
+
}
|
|
4144
|
+
const base = {
|
|
4145
|
+
schemaVersion: CLI_EVENT_SCHEMA_VERSION,
|
|
4146
|
+
kind: "run-event",
|
|
4147
|
+
sequence,
|
|
4148
|
+
type: event.type
|
|
4149
|
+
};
|
|
4150
|
+
switch (event.type) {
|
|
4151
|
+
case "text-delta":
|
|
4152
|
+
return { ...base, textDelta: event.textDelta };
|
|
4153
|
+
case "tool-call":
|
|
4154
|
+
return { ...base, toolCallId: event.toolCall.id, toolName: event.toolCall.name };
|
|
4155
|
+
case "tool-result":
|
|
4156
|
+
return {
|
|
4157
|
+
...base,
|
|
4158
|
+
toolCallId: event.toolResult.toolCallId,
|
|
4159
|
+
toolName: event.toolResult.toolName,
|
|
4160
|
+
isError: event.toolResult.isError
|
|
4161
|
+
};
|
|
4162
|
+
case "tool-approval-request":
|
|
4163
|
+
case "agent-approval-request":
|
|
4164
|
+
return {
|
|
4165
|
+
...base,
|
|
4166
|
+
approvalRequestId: event.approval.id,
|
|
4167
|
+
approvalKind: event.approval.kind ?? "provider",
|
|
4168
|
+
toolName: event.approval.name
|
|
4169
|
+
};
|
|
4170
|
+
case "agent-approval-resolved":
|
|
4171
|
+
return {
|
|
4172
|
+
...base,
|
|
4173
|
+
approvalRequestId: event.approval.approvalRequestId,
|
|
4174
|
+
approved: event.approval.approve
|
|
4175
|
+
};
|
|
4176
|
+
case "provider-data":
|
|
4177
|
+
return { ...base, provider: event.provider };
|
|
4178
|
+
case "image-generation":
|
|
4179
|
+
return {
|
|
4180
|
+
...base,
|
|
4181
|
+
provider: event.provider,
|
|
4182
|
+
partial: event.partial,
|
|
4183
|
+
...event.id ? { id: event.id } : {},
|
|
4184
|
+
...event.index !== undefined ? { index: event.index } : {}
|
|
4185
|
+
};
|
|
4186
|
+
case "finish":
|
|
4187
|
+
return {
|
|
4188
|
+
...base,
|
|
4189
|
+
...event.finishReason ? { finishReason: event.finishReason } : {},
|
|
4190
|
+
...event.usage ? { usage: event.usage } : {}
|
|
4191
|
+
};
|
|
4192
|
+
case "error":
|
|
4193
|
+
return { ...base, error: "Provider stream failed." };
|
|
4194
|
+
case "agent-run-start":
|
|
4195
|
+
return { ...base, currentStep: event.currentStep, maxSteps: event.maxSteps };
|
|
4196
|
+
case "agent-step-start":
|
|
4197
|
+
return { ...base, stepIndex: event.stepIndex };
|
|
4198
|
+
case "agent-step-finish":
|
|
4199
|
+
return {
|
|
4200
|
+
...base,
|
|
4201
|
+
stepIndex: event.step.index,
|
|
4202
|
+
status: event.step.status,
|
|
4203
|
+
toolCalls: event.step.toolResults.length
|
|
4204
|
+
};
|
|
4205
|
+
case "agent-compaction":
|
|
4206
|
+
return {
|
|
4207
|
+
...base,
|
|
4208
|
+
compactionId: event.compaction.id,
|
|
4209
|
+
reasons: event.compaction.reasons,
|
|
4210
|
+
messageCountBefore: event.compaction.messageCountBefore,
|
|
4211
|
+
messageCountAfter: event.compaction.messageCountAfter
|
|
4212
|
+
};
|
|
4213
|
+
case "agent-run-finish":
|
|
4214
|
+
return {
|
|
4215
|
+
...base,
|
|
4216
|
+
runId: event.state.runId,
|
|
4217
|
+
status: event.status,
|
|
4218
|
+
provider: event.state.provider,
|
|
4219
|
+
model: event.state.modelId
|
|
4220
|
+
};
|
|
4221
|
+
}
|
|
4222
|
+
};
|
|
4223
|
+
var serializeStreamEvent = (event, sequence = 0) => JSON.stringify(streamEventDocument(event, sequence));
|
|
4224
|
+
var streamResultDocument = (result, sequence = 0) => {
|
|
4225
|
+
if (!Number.isSafeInteger(sequence) || sequence < 0) {
|
|
4226
|
+
throw new Error("JSONL result sequence must be a non-negative integer.");
|
|
4227
|
+
}
|
|
4228
|
+
return {
|
|
4229
|
+
schemaVersion: CLI_EVENT_SCHEMA_VERSION,
|
|
4230
|
+
kind: "run-result",
|
|
4231
|
+
sequence,
|
|
4232
|
+
runId: result.runId,
|
|
4233
|
+
status: result.status,
|
|
4234
|
+
provider: result.provider,
|
|
4235
|
+
model: result.model,
|
|
4236
|
+
steps: result.steps,
|
|
4237
|
+
toolCalls: result.toolCalls,
|
|
4238
|
+
pendingApprovals: (result.pendingApprovals ?? []).map((approval) => ({
|
|
4239
|
+
id: approval.id,
|
|
4240
|
+
kind: approval.kind,
|
|
4241
|
+
name: approval.name,
|
|
4242
|
+
...approval.childRunId ? { childRunId: approval.childRunId } : {},
|
|
4243
|
+
...approval.childAgentId ? { childAgentId: approval.childAgentId } : {}
|
|
4244
|
+
})),
|
|
4245
|
+
children: (result.children ?? []).map((child) => ({
|
|
4246
|
+
runId: child.runId,
|
|
4247
|
+
status: child.status
|
|
4248
|
+
}))
|
|
4249
|
+
};
|
|
4250
|
+
};
|
|
4251
|
+
var serializeStreamResult = (result, sequence = 0) => JSON.stringify(streamResultDocument(result, sequence));
|
|
4252
|
+
|
|
4253
|
+
// src/sessions.ts
|
|
4254
|
+
import { createHash as createHash8, randomUUID as randomUUID4 } from "crypto";
|
|
4255
|
+
import { chmod as chmod3, lstat as lstat6, mkdir as mkdir4, open as open3, realpath as realpath4, stat as stat3 } from "fs/promises";
|
|
4256
|
+
import path8 from "path";
|
|
4257
|
+
import { createRedactionPolicy as createRedactionPolicy3 } from "@zhivex-ai/agents";
|
|
4258
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
4259
|
+
var HARNESS_SESSION_INDEX_FILE = "operations.sqlite";
|
|
4260
|
+
var HARNESS_SESSION_SCHEMA_VERSION = 1;
|
|
4261
|
+
var SESSION_RUN_STATUSES = [
|
|
4262
|
+
"created",
|
|
4263
|
+
"running",
|
|
4264
|
+
"waiting_approval",
|
|
4265
|
+
"cancel_requested",
|
|
4266
|
+
"completed",
|
|
4267
|
+
"failed",
|
|
4268
|
+
"cancelled",
|
|
4269
|
+
"timed_out"
|
|
4270
|
+
];
|
|
4271
|
+
var TERMINAL_SESSION_RUN_STATUSES = [
|
|
4272
|
+
"completed",
|
|
4273
|
+
"failed",
|
|
4274
|
+
"cancelled",
|
|
4275
|
+
"timed_out"
|
|
4276
|
+
];
|
|
4277
|
+
var DEFAULT_RETENTION_MS = 90 * 24 * 60 * 60 * 1000;
|
|
4278
|
+
var DEFAULT_MAX_SESSIONS = 200;
|
|
4279
|
+
var DEFAULT_MAX_RUNS = 1000;
|
|
4280
|
+
var DEFAULT_MAX_METADATA_BYTES = 4096;
|
|
4281
|
+
var DEFAULT_MAX_INDEX_BYTES = 16 * 1024 * 1024;
|
|
4282
|
+
var MAX_LIST_LIMIT = 200;
|
|
4283
|
+
var ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/+\-]{0,255}$/;
|
|
4284
|
+
var TERMINAL_STATUSES2 = new Set(TERMINAL_SESSION_RUN_STATUSES);
|
|
4285
|
+
var ACTIVE_STATUSES = new Set([
|
|
4286
|
+
"created",
|
|
4287
|
+
"running",
|
|
4288
|
+
"waiting_approval",
|
|
4289
|
+
"cancel_requested"
|
|
4290
|
+
]);
|
|
4291
|
+
var STATUS_TRANSITIONS = {
|
|
4292
|
+
created: new Set(["created", "running", "waiting_approval", "completed", "failed", "cancelled", "timed_out"]),
|
|
4293
|
+
running: new Set(["running", "waiting_approval", "cancel_requested", "completed", "failed", "cancelled", "timed_out"]),
|
|
4294
|
+
waiting_approval: new Set(["waiting_approval", "running", "cancel_requested", "completed", "failed", "cancelled", "timed_out"]),
|
|
4295
|
+
cancel_requested: new Set(["cancel_requested", "cancelled", "failed", "timed_out"]),
|
|
4296
|
+
completed: new Set(["completed"]),
|
|
4297
|
+
failed: new Set(["failed"]),
|
|
4298
|
+
cancelled: new Set(["cancelled"]),
|
|
4299
|
+
timed_out: new Set(["timed_out"])
|
|
4300
|
+
};
|
|
4301
|
+
var metadataRedaction = createRedactionPolicy3({
|
|
4302
|
+
includeEmails: true,
|
|
4303
|
+
rules: [
|
|
4304
|
+
{ name: "private-key", pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g },
|
|
4305
|
+
{ name: "common-provider-key", pattern: /\b(?:sk|ghp|github_pat|xox[baprs])[-_][A-Za-z0-9_-]{8,}\b/gi },
|
|
4306
|
+
{ name: "url-credentials", pattern: /:\/\/[^\s/@:]+:[^\s/@]+@/g, replacement: "://[REDACTED]@" }
|
|
4307
|
+
]
|
|
4308
|
+
});
|
|
4309
|
+
var byteLength = (value) => Buffer.byteLength(value, "utf8");
|
|
4310
|
+
var boundedInteger = (name, value, fallback, minimum) => {
|
|
4311
|
+
const resolved = value ?? fallback;
|
|
4312
|
+
if (!Number.isSafeInteger(resolved) || resolved < minimum) {
|
|
4313
|
+
throw new Error(`${name} must be an integer greater than or equal to ${minimum}.`);
|
|
4314
|
+
}
|
|
4315
|
+
return resolved;
|
|
4316
|
+
};
|
|
4317
|
+
var assertTimestamp = (name, value) => {
|
|
4318
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
4319
|
+
throw new Error(`${name} must be a non-negative integer timestamp.`);
|
|
4320
|
+
}
|
|
4321
|
+
};
|
|
4322
|
+
var assertIdentifier = (name, value) => {
|
|
4323
|
+
if (!ID_PATTERN.test(value)) {
|
|
4324
|
+
throw new Error(`${name} contains unsupported characters or exceeds 256 bytes.`);
|
|
4325
|
+
}
|
|
4326
|
+
if (metadataRedaction.redactText(value) !== value) {
|
|
4327
|
+
throw new Error(`${name} resembles sensitive metadata and cannot be stored in the session index.`);
|
|
4328
|
+
}
|
|
4329
|
+
return value;
|
|
4330
|
+
};
|
|
4331
|
+
var assertScopeSegment = (name, value) => {
|
|
4332
|
+
const normalized = value.trim();
|
|
4333
|
+
if (!normalized || normalized.length > 128 || /[\u0000-\u001f\u007f]/.test(normalized)) {
|
|
4334
|
+
throw new Error(`${name} must contain 1-128 printable characters.`);
|
|
4335
|
+
}
|
|
4336
|
+
return normalized;
|
|
4337
|
+
};
|
|
4338
|
+
var normalizeTitle = (value, maxMetadataBytes) => {
|
|
4339
|
+
if (value === undefined || value === null) {
|
|
4340
|
+
return value;
|
|
4341
|
+
}
|
|
4342
|
+
const normalized = metadataRedaction.redactText(value).replace(/[\u0000-\u001f\u007f]+/g, " ").trim();
|
|
4343
|
+
if (!normalized) {
|
|
4344
|
+
throw new Error("Session title must not be empty.");
|
|
4345
|
+
}
|
|
4346
|
+
if (byteLength(normalized) > maxMetadataBytes) {
|
|
4347
|
+
throw new Error(`Session title exceeds the ${maxMetadataBytes}-byte metadata limit.`);
|
|
4348
|
+
}
|
|
4349
|
+
return normalized;
|
|
4350
|
+
};
|
|
4351
|
+
var normalizeScope = (scope) => {
|
|
4352
|
+
const tenantId = assertScopeSegment("scope.tenantId", scope.tenantId);
|
|
4353
|
+
const userId = scope.userId === undefined ? "" : assertScopeSegment("scope.userId", scope.userId);
|
|
4354
|
+
const namespace = scope.namespace === undefined ? "" : assertScopeSegment("scope.namespace", scope.namespace);
|
|
4355
|
+
return `${tenantId}\x00${userId}\x00${namespace}`;
|
|
4356
|
+
};
|
|
4357
|
+
var stableKey = (kind, value) => createHash8("sha256").update(kind).update("\x00").update(value).digest("hex");
|
|
4358
|
+
var sessionId = () => `ses_${randomUUID4()}`;
|
|
4359
|
+
var turnId = () => `turn_${randomUUID4()}`;
|
|
4360
|
+
var isMissing = (error) => error.code === "ENOENT";
|
|
4361
|
+
var ensurePrivateDatabase = async (workspace, stateDirectory) => {
|
|
4362
|
+
const requestedStateDirectory = path8.resolve(stateDirectory);
|
|
4363
|
+
await validateStateDirectory(workspace, requestedStateDirectory);
|
|
4364
|
+
await mkdir4(requestedStateDirectory, { recursive: true, mode: 448 });
|
|
4365
|
+
const directoryEntry = await lstat6(requestedStateDirectory);
|
|
4366
|
+
if (directoryEntry.isSymbolicLink() || !directoryEntry.isDirectory()) {
|
|
4367
|
+
throw new Error(`The session state directory must be a real directory: ${requestedStateDirectory}.`);
|
|
4368
|
+
}
|
|
4369
|
+
await chmod3(requestedStateDirectory, 448);
|
|
4370
|
+
const canonicalStateDirectory2 = await realpath4(requestedStateDirectory);
|
|
4371
|
+
const databasePath = path8.join(canonicalStateDirectory2, HARNESS_SESSION_INDEX_FILE);
|
|
4372
|
+
let databaseEntry;
|
|
4373
|
+
try {
|
|
4374
|
+
databaseEntry = await lstat6(databasePath);
|
|
4375
|
+
} catch (error) {
|
|
4376
|
+
if (!isMissing(error)) {
|
|
4377
|
+
throw error;
|
|
4378
|
+
}
|
|
4379
|
+
try {
|
|
4380
|
+
const handle = await open3(databasePath, "wx", 384);
|
|
4381
|
+
await handle.close();
|
|
4382
|
+
} catch (creationError) {
|
|
4383
|
+
if (creationError.code !== "EEXIST") {
|
|
4384
|
+
throw creationError;
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
4387
|
+
databaseEntry = await lstat6(databasePath);
|
|
4388
|
+
}
|
|
4389
|
+
if (databaseEntry.isSymbolicLink() || !databaseEntry.isFile()) {
|
|
4390
|
+
throw new Error(`The session SQLite path must be a real file: ${databasePath}.`);
|
|
4391
|
+
}
|
|
4392
|
+
await chmod3(databasePath, 384);
|
|
4393
|
+
return { databasePath, databaseEntry };
|
|
4394
|
+
};
|
|
4395
|
+
var toRunReference = (row) => ({
|
|
4396
|
+
turnId: row.turn_id,
|
|
4397
|
+
...row.source_turn_id ? { sourceTurnId: row.source_turn_id } : {},
|
|
4398
|
+
sequence: row.ordinal,
|
|
4399
|
+
runId: row.run_id,
|
|
4400
|
+
provider: row.provider,
|
|
4401
|
+
model: row.model,
|
|
4402
|
+
...row.role ? { role: row.role } : {},
|
|
4403
|
+
status: row.status,
|
|
4404
|
+
createdAt: row.created_at,
|
|
4405
|
+
updatedAt: row.updated_at,
|
|
4406
|
+
...row.completed_at === null ? {} : { completedAt: row.completed_at }
|
|
4407
|
+
});
|
|
4408
|
+
var metadataSize = (input) => Object.values(input).reduce((total, value) => total + (typeof value === "string" ? byteLength(value) : 0), 0);
|
|
4409
|
+
var isTerminal = (status) => TERMINAL_STATUSES2.has(status);
|
|
4410
|
+
var openCliSessionStore = async (options) => {
|
|
4411
|
+
const workspace = await realpath4(path8.resolve(options.workspace));
|
|
4412
|
+
const workspaceEntry = await stat3(workspace);
|
|
4413
|
+
if (!workspaceEntry.isDirectory()) {
|
|
4414
|
+
throw new Error(`The session workspace is not a directory: ${workspace}.`);
|
|
4415
|
+
}
|
|
4416
|
+
const scopeValue2 = normalizeScope(options.scope);
|
|
4417
|
+
const workspaceKey = stableKey("workspace", workspace);
|
|
4418
|
+
const scopeKey = stableKey("scope", scopeValue2);
|
|
4419
|
+
const retentionMs = boundedInteger("retentionMs", options.retentionMs, DEFAULT_RETENTION_MS, 0);
|
|
4420
|
+
const maxSessions = boundedInteger("maxSessionsPerWorkspace", options.maxSessionsPerWorkspace, DEFAULT_MAX_SESSIONS, 1);
|
|
4421
|
+
const maxRuns = boundedInteger("maxRunsPerSession", options.maxRunsPerSession, DEFAULT_MAX_RUNS, 1);
|
|
4422
|
+
const maxMetadataBytes = boundedInteger("maxMetadataBytes", options.maxMetadataBytes, DEFAULT_MAX_METADATA_BYTES, 32);
|
|
4423
|
+
const maxIndexBytes = boundedInteger("maxIndexBytes", options.maxIndexBytes, DEFAULT_MAX_INDEX_BYTES, 1024);
|
|
4424
|
+
const now = options.now ?? Date.now;
|
|
4425
|
+
const { databasePath, databaseEntry } = await ensurePrivateDatabase(workspace, options.stateDirectory);
|
|
4426
|
+
const database = new Database2(databasePath, { create: false, strict: true });
|
|
4427
|
+
let closed = false;
|
|
4428
|
+
const assertOpen = () => {
|
|
4429
|
+
if (closed) {
|
|
4430
|
+
throw new Error("The CLI session store is closed.");
|
|
4431
|
+
}
|
|
4432
|
+
};
|
|
4433
|
+
const transaction = (operation) => {
|
|
4434
|
+
assertOpen();
|
|
4435
|
+
database.exec("BEGIN IMMEDIATE");
|
|
4436
|
+
try {
|
|
4437
|
+
const result = operation();
|
|
4438
|
+
const size = database.query(`
|
|
4439
|
+
SELECT
|
|
4440
|
+
COALESCE((SELECT SUM(metadata_bytes) FROM zhivex_cli_sessions WHERE workspace_key = ?1 AND scope_key = ?2), 0) +
|
|
4441
|
+
COALESCE((SELECT SUM(r.metadata_bytes) FROM zhivex_cli_session_runs r JOIN zhivex_cli_sessions s ON s.session_id = r.session_id WHERE s.workspace_key = ?1 AND s.scope_key = ?2), 0)
|
|
4442
|
+
AS size
|
|
4443
|
+
`).get(workspaceKey, scopeKey)?.size ?? 0;
|
|
4444
|
+
if (size > maxIndexBytes) {
|
|
4445
|
+
throw new Error(`Session index metadata exceeds the ${maxIndexBytes}-byte limit.`);
|
|
4446
|
+
}
|
|
4447
|
+
database.exec("COMMIT");
|
|
4448
|
+
return result;
|
|
4449
|
+
} catch (error) {
|
|
4450
|
+
try {
|
|
4451
|
+
database.exec("ROLLBACK");
|
|
4452
|
+
} catch {}
|
|
4453
|
+
throw error;
|
|
4454
|
+
}
|
|
4455
|
+
};
|
|
4456
|
+
const ensureRevision = (row, expected) => {
|
|
4457
|
+
if (expected !== undefined && row.revision !== expected) {
|
|
4458
|
+
throw new Error(`Session ${row.session_id} changed concurrently (expected revision ${expected}, found ${row.revision}).`);
|
|
4459
|
+
}
|
|
4460
|
+
};
|
|
4461
|
+
const selectSessionRow = (candidateSessionId, includeDeleted = false) => {
|
|
4462
|
+
assertIdentifier("sessionId", candidateSessionId);
|
|
4463
|
+
return database.query(`
|
|
4464
|
+
SELECT session_id, title, parent_session_id, forked_from_turn_id, revision, activity_seq,
|
|
4465
|
+
created_at, updated_at, archived_at, deleted_at
|
|
4466
|
+
FROM zhivex_cli_sessions
|
|
4467
|
+
WHERE session_id = ?1 AND workspace_key = ?2 AND scope_key = ?3
|
|
4468
|
+
${includeDeleted ? "" : "AND deleted_at IS NULL"}
|
|
4469
|
+
`).get(candidateSessionId, workspaceKey, scopeKey);
|
|
4470
|
+
};
|
|
4471
|
+
const requireSessionRow = (candidateSessionId, includeDeleted = false) => {
|
|
4472
|
+
const row = selectSessionRow(candidateSessionId, includeDeleted);
|
|
4473
|
+
if (!row) {
|
|
4474
|
+
throw new Error(`Session ${candidateSessionId} was not found in this workspace and scope.`);
|
|
4475
|
+
}
|
|
4476
|
+
return row;
|
|
4477
|
+
};
|
|
4478
|
+
const selectRuns = (candidateSessionId) => database.query(`
|
|
4479
|
+
SELECT turn_id, source_turn_id, ordinal, run_id, provider, model, role, status,
|
|
4480
|
+
created_at, updated_at, completed_at
|
|
4481
|
+
FROM zhivex_cli_session_runs
|
|
4482
|
+
WHERE session_id = ?1
|
|
4483
|
+
ORDER BY ordinal ASC
|
|
4484
|
+
`).all(candidateSessionId).map(toRunReference);
|
|
4485
|
+
const nextActivitySequence = () => database.query(`
|
|
4486
|
+
SELECT COALESCE(MAX(activity_seq), 0) + 1 AS count
|
|
4487
|
+
FROM zhivex_cli_sessions
|
|
4488
|
+
WHERE workspace_key = ?1 AND scope_key = ?2
|
|
4489
|
+
`).get(workspaceKey, scopeKey)?.count ?? 1;
|
|
4490
|
+
const sessionBase = (row) => ({
|
|
4491
|
+
schemaVersion: HARNESS_SESSION_SCHEMA_VERSION,
|
|
4492
|
+
sessionId: row.session_id,
|
|
4493
|
+
workspaceKey,
|
|
4494
|
+
scopeKey,
|
|
4495
|
+
...row.title ? { title: row.title } : {},
|
|
4496
|
+
...row.parent_session_id ? { parentSessionId: row.parent_session_id } : {},
|
|
4497
|
+
...row.forked_from_turn_id ? { forkedFromTurnId: row.forked_from_turn_id } : {},
|
|
4498
|
+
revision: row.revision,
|
|
4499
|
+
createdAt: row.created_at,
|
|
4500
|
+
updatedAt: row.updated_at,
|
|
4501
|
+
...row.archived_at === null ? {} : { archivedAt: row.archived_at },
|
|
4502
|
+
...row.deleted_at === null ? {} : { deletedAt: row.deleted_at }
|
|
4503
|
+
});
|
|
4504
|
+
const hydrate = (row) => ({
|
|
4505
|
+
...sessionBase(row),
|
|
4506
|
+
runs: selectRuns(row.session_id)
|
|
4507
|
+
});
|
|
4508
|
+
const latestRunRow = (candidateSessionId) => database.query(`
|
|
4509
|
+
SELECT turn_id, source_turn_id, ordinal, run_id, provider, model, role, status,
|
|
4510
|
+
created_at, updated_at, completed_at
|
|
4511
|
+
FROM zhivex_cli_session_runs
|
|
4512
|
+
WHERE session_id = ?1
|
|
4513
|
+
ORDER BY ordinal DESC
|
|
4514
|
+
LIMIT 1
|
|
4515
|
+
`).get(candidateSessionId);
|
|
4516
|
+
const assertNoActiveRun = (candidateSessionId, action) => {
|
|
4517
|
+
const latest = latestRunRow(candidateSessionId);
|
|
4518
|
+
if (latest && ACTIVE_STATUSES.has(latest.status)) {
|
|
4519
|
+
throw new Error(`Cannot ${action} session ${candidateSessionId} while run ${latest.run_id} is ${latest.status}.`);
|
|
4520
|
+
}
|
|
4521
|
+
};
|
|
4522
|
+
const insertRun = (candidateSessionId, input, sequence, sourceTurnId) => {
|
|
4523
|
+
const runId = assertIdentifier("runId", input.runId);
|
|
4524
|
+
const provider = assertIdentifier("provider", input.provider);
|
|
4525
|
+
const model = assertIdentifier("model", input.model);
|
|
4526
|
+
const role = input.role === undefined ? null : assertIdentifier("role", input.role);
|
|
4527
|
+
const status = input.status ?? "created";
|
|
4528
|
+
if (!SESSION_RUN_STATUSES.includes(status)) {
|
|
4529
|
+
throw new Error(`Unsupported session run status: ${String(status)}.`);
|
|
4530
|
+
}
|
|
4531
|
+
const createdAt = input.createdAt ?? now();
|
|
4532
|
+
assertTimestamp("createdAt", createdAt);
|
|
4533
|
+
const entryBytes = metadataSize({ runId, provider, model, role });
|
|
4534
|
+
if (entryBytes > maxMetadataBytes) {
|
|
4535
|
+
throw new Error(`Run reference metadata exceeds the ${maxMetadataBytes}-byte limit.`);
|
|
4536
|
+
}
|
|
4537
|
+
const candidateTurnId = turnId();
|
|
4538
|
+
database.query(`
|
|
4539
|
+
INSERT INTO zhivex_cli_session_runs (
|
|
4540
|
+
turn_id, source_turn_id, session_id, ordinal, run_id, provider, model, role,
|
|
4541
|
+
status, created_at, updated_at, completed_at, metadata_bytes
|
|
4542
|
+
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?10, ?11, ?12)
|
|
4543
|
+
`).run(candidateTurnId, sourceTurnId ?? null, candidateSessionId, sequence, runId, provider, model, role, status, createdAt, isTerminal(status) ? createdAt : null, entryBytes);
|
|
4544
|
+
};
|
|
4545
|
+
try {
|
|
4546
|
+
const openedEntry = await lstat6(databasePath);
|
|
4547
|
+
if (openedEntry.isSymbolicLink() || !openedEntry.isFile() || openedEntry.dev !== databaseEntry.dev || openedEntry.ino !== databaseEntry.ino) {
|
|
4548
|
+
throw new Error(`The session SQLite path changed while it was being opened: ${databasePath}.`);
|
|
4549
|
+
}
|
|
4550
|
+
database.exec("PRAGMA journal_mode = WAL");
|
|
4551
|
+
database.exec("PRAGMA busy_timeout = 5000");
|
|
4552
|
+
database.exec("PRAGMA foreign_keys = ON");
|
|
4553
|
+
database.exec(`
|
|
4554
|
+
CREATE TABLE IF NOT EXISTS zhivex_cli_session_schema (
|
|
4555
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
4556
|
+
version INTEGER NOT NULL
|
|
4557
|
+
);
|
|
4558
|
+
INSERT OR IGNORE INTO zhivex_cli_session_schema (singleton, version)
|
|
4559
|
+
VALUES (1, ${HARNESS_SESSION_SCHEMA_VERSION});
|
|
4560
|
+
CREATE TABLE IF NOT EXISTS zhivex_cli_sessions (
|
|
4561
|
+
session_id TEXT PRIMARY KEY,
|
|
4562
|
+
workspace_key TEXT NOT NULL,
|
|
4563
|
+
scope_key TEXT NOT NULL,
|
|
4564
|
+
title TEXT,
|
|
4565
|
+
parent_session_id TEXT,
|
|
4566
|
+
forked_from_turn_id TEXT,
|
|
4567
|
+
revision INTEGER NOT NULL DEFAULT 0,
|
|
4568
|
+
activity_seq INTEGER NOT NULL,
|
|
4569
|
+
created_at INTEGER NOT NULL,
|
|
4570
|
+
updated_at INTEGER NOT NULL,
|
|
4571
|
+
archived_at INTEGER,
|
|
4572
|
+
deleted_at INTEGER,
|
|
4573
|
+
metadata_bytes INTEGER NOT NULL,
|
|
4574
|
+
CHECK (length(session_id) <= 260),
|
|
4575
|
+
CHECK (metadata_bytes >= 0)
|
|
4576
|
+
);
|
|
4577
|
+
CREATE INDEX IF NOT EXISTS zhivex_cli_sessions_latest
|
|
4578
|
+
ON zhivex_cli_sessions (workspace_key, scope_key, deleted_at, archived_at, activity_seq DESC);
|
|
4579
|
+
CREATE TABLE IF NOT EXISTS zhivex_cli_session_runs (
|
|
4580
|
+
turn_id TEXT PRIMARY KEY,
|
|
4581
|
+
source_turn_id TEXT,
|
|
4582
|
+
session_id TEXT NOT NULL REFERENCES zhivex_cli_sessions(session_id) ON DELETE CASCADE,
|
|
4583
|
+
ordinal INTEGER NOT NULL,
|
|
4584
|
+
run_id TEXT NOT NULL,
|
|
4585
|
+
provider TEXT NOT NULL,
|
|
4586
|
+
model TEXT NOT NULL,
|
|
4587
|
+
role TEXT,
|
|
4588
|
+
status TEXT NOT NULL CHECK (status IN ('created', 'running', 'waiting_approval', 'cancel_requested', 'completed', 'failed', 'cancelled', 'timed_out')),
|
|
4589
|
+
created_at INTEGER NOT NULL,
|
|
4590
|
+
updated_at INTEGER NOT NULL,
|
|
4591
|
+
completed_at INTEGER,
|
|
4592
|
+
metadata_bytes INTEGER NOT NULL,
|
|
4593
|
+
UNIQUE (session_id, ordinal),
|
|
4594
|
+
UNIQUE (session_id, run_id),
|
|
4595
|
+
CHECK (ordinal >= 0),
|
|
4596
|
+
CHECK (metadata_bytes >= 0)
|
|
4597
|
+
);
|
|
4598
|
+
CREATE INDEX IF NOT EXISTS zhivex_cli_session_runs_latest
|
|
4599
|
+
ON zhivex_cli_session_runs (session_id, ordinal DESC);
|
|
4600
|
+
`);
|
|
4601
|
+
const schemaVersion = database.query("SELECT version FROM zhivex_cli_session_schema WHERE singleton = 1").get()?.version;
|
|
4602
|
+
if (schemaVersion !== HARNESS_SESSION_SCHEMA_VERSION) {
|
|
4603
|
+
throw new Error(`Unsupported CLI session schema version: ${String(schemaVersion)}.`);
|
|
4604
|
+
}
|
|
4605
|
+
await chmod3(databasePath, 384);
|
|
4606
|
+
} catch (error) {
|
|
4607
|
+
database.close(false);
|
|
4608
|
+
throw error;
|
|
4609
|
+
}
|
|
4610
|
+
const prune = async (input = {}) => transaction(() => {
|
|
4611
|
+
const before = input.before ?? Math.max(0, now() - retentionMs);
|
|
4612
|
+
assertTimestamp("before", before);
|
|
4613
|
+
const deletedSessions = database.query(`
|
|
4614
|
+
SELECT COUNT(*) AS count FROM zhivex_cli_sessions
|
|
4615
|
+
WHERE workspace_key = ?1 AND scope_key = ?2
|
|
4616
|
+
AND updated_at < ?3
|
|
4617
|
+
AND (archived_at IS NOT NULL OR deleted_at IS NOT NULL)
|
|
4618
|
+
`).get(workspaceKey, scopeKey, before)?.count ?? 0;
|
|
4619
|
+
database.query(`
|
|
4620
|
+
DELETE FROM zhivex_cli_sessions
|
|
4621
|
+
WHERE workspace_key = ?1 AND scope_key = ?2
|
|
4622
|
+
AND updated_at < ?3
|
|
4623
|
+
AND (archived_at IS NOT NULL OR deleted_at IS NOT NULL)
|
|
4624
|
+
`).run(workspaceKey, scopeKey, before);
|
|
4625
|
+
return { before, deletedSessions };
|
|
4626
|
+
});
|
|
4627
|
+
const store = {
|
|
4628
|
+
databasePath,
|
|
4629
|
+
workspaceKey,
|
|
4630
|
+
scopeKey,
|
|
4631
|
+
async create(input = {}) {
|
|
4632
|
+
return transaction(() => {
|
|
4633
|
+
const cutoff = Math.max(0, now() - retentionMs);
|
|
4634
|
+
database.query(`
|
|
4635
|
+
DELETE FROM zhivex_cli_sessions
|
|
4636
|
+
WHERE workspace_key = ?1 AND scope_key = ?2 AND updated_at < ?3
|
|
4637
|
+
AND (archived_at IS NOT NULL OR deleted_at IS NOT NULL)
|
|
4638
|
+
`).run(workspaceKey, scopeKey, cutoff);
|
|
4639
|
+
const count = database.query(`
|
|
4640
|
+
SELECT COUNT(*) AS count FROM zhivex_cli_sessions
|
|
4641
|
+
WHERE workspace_key = ?1 AND scope_key = ?2
|
|
4642
|
+
`).get(workspaceKey, scopeKey)?.count ?? 0;
|
|
4643
|
+
if (count >= maxSessions) {
|
|
4644
|
+
throw new Error(`Session limit reached for this workspace and scope (${maxSessions}). Archive and prune an older session.`);
|
|
4645
|
+
}
|
|
4646
|
+
const createdAt = now();
|
|
4647
|
+
assertTimestamp("now", createdAt);
|
|
4648
|
+
const title = normalizeTitle(input.title, maxMetadataBytes) ?? null;
|
|
4649
|
+
const candidateSessionId = sessionId();
|
|
4650
|
+
const activitySequence = nextActivitySequence();
|
|
4651
|
+
database.query(`
|
|
4652
|
+
INSERT INTO zhivex_cli_sessions (
|
|
4653
|
+
session_id, workspace_key, scope_key, title, revision, created_at,
|
|
4654
|
+
updated_at, activity_seq, metadata_bytes
|
|
4655
|
+
) VALUES (?1, ?2, ?3, ?4, 0, ?5, ?5, ?6, ?7)
|
|
4656
|
+
`).run(candidateSessionId, workspaceKey, scopeKey, title, createdAt, activitySequence, metadataSize({ title }));
|
|
4657
|
+
if (input.initialRun) {
|
|
4658
|
+
insertRun(candidateSessionId, input.initialRun, 0);
|
|
4659
|
+
}
|
|
4660
|
+
return hydrate(requireSessionRow(candidateSessionId));
|
|
4661
|
+
});
|
|
4662
|
+
},
|
|
4663
|
+
async get(candidateSessionId, input = {}) {
|
|
4664
|
+
assertOpen();
|
|
4665
|
+
const row = selectSessionRow(candidateSessionId, input.includeDeleted ?? false);
|
|
4666
|
+
return row ? hydrate(row) : undefined;
|
|
4667
|
+
},
|
|
4668
|
+
async list(query = {}) {
|
|
4669
|
+
assertOpen();
|
|
4670
|
+
const limit = boundedInteger("limit", query.limit, 50, 1);
|
|
4671
|
+
if (limit > MAX_LIST_LIMIT) {
|
|
4672
|
+
throw new Error(`limit cannot exceed ${MAX_LIST_LIMIT}.`);
|
|
4673
|
+
}
|
|
4674
|
+
const rows = database.query(`
|
|
4675
|
+
SELECT session_id, title, parent_session_id, forked_from_turn_id, revision, activity_seq,
|
|
4676
|
+
created_at, updated_at, archived_at, deleted_at
|
|
4677
|
+
FROM zhivex_cli_sessions
|
|
4678
|
+
WHERE workspace_key = ?1 AND scope_key = ?2
|
|
4679
|
+
${query.includeArchived ? "" : "AND archived_at IS NULL"}
|
|
4680
|
+
${query.includeDeleted ? "" : "AND deleted_at IS NULL"}
|
|
4681
|
+
ORDER BY activity_seq DESC
|
|
4682
|
+
LIMIT ?3
|
|
4683
|
+
`).all(workspaceKey, scopeKey, limit);
|
|
4684
|
+
return rows.map((row) => {
|
|
4685
|
+
const latest = latestRunRow(row.session_id);
|
|
4686
|
+
const runCount = database.query("SELECT COUNT(*) AS count FROM zhivex_cli_session_runs WHERE session_id = ?1").get(row.session_id)?.count ?? 0;
|
|
4687
|
+
return {
|
|
4688
|
+
...sessionBase(row),
|
|
4689
|
+
runCount,
|
|
4690
|
+
...latest ? { latestRun: toRunReference(latest) } : {}
|
|
4691
|
+
};
|
|
4692
|
+
});
|
|
4693
|
+
},
|
|
4694
|
+
async latest(input = {}) {
|
|
4695
|
+
assertOpen();
|
|
4696
|
+
const row = database.query(`
|
|
4697
|
+
SELECT session_id, title, parent_session_id, forked_from_turn_id, revision, activity_seq,
|
|
4698
|
+
created_at, updated_at, archived_at, deleted_at
|
|
4699
|
+
FROM zhivex_cli_sessions
|
|
4700
|
+
WHERE workspace_key = ?1 AND scope_key = ?2 AND deleted_at IS NULL
|
|
4701
|
+
${input.includeArchived ? "" : "AND archived_at IS NULL"}
|
|
4702
|
+
ORDER BY activity_seq DESC
|
|
4703
|
+
LIMIT 1
|
|
4704
|
+
`).get(workspaceKey, scopeKey);
|
|
4705
|
+
return row ? hydrate(row) : undefined;
|
|
4706
|
+
},
|
|
4707
|
+
async update(candidateSessionId, input) {
|
|
4708
|
+
return transaction(() => {
|
|
4709
|
+
const row = requireSessionRow(candidateSessionId);
|
|
4710
|
+
ensureRevision(row, input.expectedRevision);
|
|
4711
|
+
if (input.archived === true) {
|
|
4712
|
+
assertNoActiveRun(candidateSessionId, "archive");
|
|
4713
|
+
}
|
|
4714
|
+
const title = input.title === undefined ? row.title : normalizeTitle(input.title, maxMetadataBytes) ?? null;
|
|
4715
|
+
const updatedAt = now();
|
|
4716
|
+
assertTimestamp("now", updatedAt);
|
|
4717
|
+
const archivedAt = input.archived === undefined ? row.archived_at : input.archived ? updatedAt : null;
|
|
4718
|
+
const activitySequence = nextActivitySequence();
|
|
4719
|
+
database.query(`
|
|
4720
|
+
UPDATE zhivex_cli_sessions
|
|
4721
|
+
SET title = ?1, archived_at = ?2, updated_at = ?3, revision = revision + 1,
|
|
4722
|
+
metadata_bytes = ?4, activity_seq = ?5
|
|
4723
|
+
WHERE session_id = ?6 AND workspace_key = ?7 AND scope_key = ?8 AND revision = ?9
|
|
4724
|
+
`).run(title, archivedAt, updatedAt, metadataSize({ title }), activitySequence, candidateSessionId, workspaceKey, scopeKey, row.revision);
|
|
4725
|
+
return hydrate(requireSessionRow(candidateSessionId));
|
|
4726
|
+
});
|
|
4727
|
+
},
|
|
4728
|
+
async rename(candidateSessionId, title, input = {}) {
|
|
4729
|
+
return store.update(candidateSessionId, {
|
|
4730
|
+
title,
|
|
4731
|
+
...input.expectedRevision === undefined ? {} : { expectedRevision: input.expectedRevision }
|
|
4732
|
+
});
|
|
4733
|
+
},
|
|
4734
|
+
async appendRun(candidateSessionId, input, appendOptions = {}) {
|
|
4735
|
+
return transaction(() => {
|
|
4736
|
+
const row = requireSessionRow(candidateSessionId);
|
|
4737
|
+
ensureRevision(row, appendOptions.expectedRevision);
|
|
4738
|
+
if (row.archived_at !== null) {
|
|
4739
|
+
throw new Error(`Cannot append a run to archived session ${candidateSessionId}.`);
|
|
4740
|
+
}
|
|
4741
|
+
const existing = database.query(`
|
|
4742
|
+
SELECT turn_id, source_turn_id, ordinal, run_id, provider, model, role, status,
|
|
4743
|
+
created_at, updated_at, completed_at
|
|
4744
|
+
FROM zhivex_cli_session_runs WHERE session_id = ?1 AND run_id = ?2
|
|
4745
|
+
`).get(candidateSessionId, input.runId);
|
|
4746
|
+
if (existing) {
|
|
4747
|
+
const requestedStatus = input.status ?? "created";
|
|
4748
|
+
if (existing.provider === input.provider && existing.model === input.model && (existing.role ?? undefined) === input.role && existing.status === requestedStatus) {
|
|
4749
|
+
return hydrate(row);
|
|
4750
|
+
}
|
|
4751
|
+
throw new Error(`Run ${input.runId} is already immutably bound to ${existing.provider}/${existing.model} in this session.`);
|
|
4752
|
+
}
|
|
4753
|
+
assertNoActiveRun(candidateSessionId, "start another run in");
|
|
4754
|
+
const count = database.query("SELECT COUNT(*) AS count FROM zhivex_cli_session_runs WHERE session_id = ?1").get(candidateSessionId)?.count ?? 0;
|
|
4755
|
+
if (count >= maxRuns) {
|
|
4756
|
+
throw new Error(`Run-reference limit reached for session ${candidateSessionId} (${maxRuns}).`);
|
|
4757
|
+
}
|
|
4758
|
+
insertRun(candidateSessionId, input, count);
|
|
4759
|
+
const updatedAt = now();
|
|
4760
|
+
assertTimestamp("now", updatedAt);
|
|
4761
|
+
const activitySequence = nextActivitySequence();
|
|
4762
|
+
database.query(`
|
|
4763
|
+
UPDATE zhivex_cli_sessions SET updated_at = ?1, activity_seq = ?2, revision = revision + 1
|
|
4764
|
+
WHERE session_id = ?3 AND revision = ?4
|
|
4765
|
+
`).run(updatedAt, activitySequence, candidateSessionId, row.revision);
|
|
4766
|
+
return hydrate(requireSessionRow(candidateSessionId));
|
|
4767
|
+
});
|
|
4768
|
+
},
|
|
4769
|
+
async updateRun(candidateSessionId, runId, input) {
|
|
4770
|
+
return transaction(() => {
|
|
4771
|
+
const session = requireSessionRow(candidateSessionId);
|
|
4772
|
+
ensureRevision(session, input.expectedSessionRevision);
|
|
4773
|
+
assertIdentifier("runId", runId);
|
|
4774
|
+
const run = database.query(`
|
|
4775
|
+
SELECT turn_id, source_turn_id, ordinal, run_id, provider, model, role, status,
|
|
4776
|
+
created_at, updated_at, completed_at
|
|
4777
|
+
FROM zhivex_cli_session_runs WHERE session_id = ?1 AND run_id = ?2
|
|
4778
|
+
`).get(candidateSessionId, runId);
|
|
4779
|
+
if (!run) {
|
|
4780
|
+
throw new Error(`Run ${runId} is not linked to session ${candidateSessionId}.`);
|
|
4781
|
+
}
|
|
4782
|
+
if (!STATUS_TRANSITIONS[run.status].has(input.status)) {
|
|
4783
|
+
throw new Error(`Run ${runId} cannot transition from ${run.status} to ${input.status}.`);
|
|
4784
|
+
}
|
|
4785
|
+
const updatedAt = input.updatedAt ?? now();
|
|
4786
|
+
assertTimestamp("updatedAt", updatedAt);
|
|
4787
|
+
if (updatedAt < run.updated_at) {
|
|
4788
|
+
throw new Error(`Run ${runId} update timestamp cannot move backwards.`);
|
|
4789
|
+
}
|
|
4790
|
+
const completedAt = isTerminal(input.status) ? input.completedAt ?? updatedAt : null;
|
|
4791
|
+
if (completedAt !== null) {
|
|
4792
|
+
assertTimestamp("completedAt", completedAt);
|
|
4793
|
+
if (completedAt < run.created_at || completedAt > updatedAt) {
|
|
4794
|
+
throw new Error("completedAt must be between the run creation and update timestamps.");
|
|
4795
|
+
}
|
|
4796
|
+
}
|
|
4797
|
+
database.query(`
|
|
4798
|
+
UPDATE zhivex_cli_session_runs
|
|
4799
|
+
SET status = ?1, updated_at = ?2, completed_at = ?3
|
|
4800
|
+
WHERE session_id = ?4 AND run_id = ?5
|
|
4801
|
+
`).run(input.status, updatedAt, completedAt, candidateSessionId, runId);
|
|
4802
|
+
const activitySequence = nextActivitySequence();
|
|
4803
|
+
database.query(`
|
|
4804
|
+
UPDATE zhivex_cli_sessions SET updated_at = ?1, activity_seq = ?2, revision = revision + 1
|
|
4805
|
+
WHERE session_id = ?3 AND revision = ?4
|
|
4806
|
+
`).run(updatedAt, activitySequence, candidateSessionId, session.revision);
|
|
4807
|
+
return hydrate(requireSessionRow(candidateSessionId));
|
|
4808
|
+
});
|
|
4809
|
+
},
|
|
4810
|
+
async fork(candidateSessionId, input = {}) {
|
|
4811
|
+
return transaction(() => {
|
|
4812
|
+
const source = requireSessionRow(candidateSessionId);
|
|
4813
|
+
ensureRevision(source, input.expectedRevision);
|
|
4814
|
+
const sourceRuns = selectRuns(candidateSessionId);
|
|
4815
|
+
let forkRuns = sourceRuns;
|
|
4816
|
+
let forkedFromTurnId;
|
|
4817
|
+
if (input.atTurnId) {
|
|
4818
|
+
const index = sourceRuns.findIndex((run) => run.turnId === input.atTurnId);
|
|
4819
|
+
if (index < 0) {
|
|
4820
|
+
throw new Error(`Turn ${input.atTurnId} is not part of session ${candidateSessionId}.`);
|
|
4821
|
+
}
|
|
4822
|
+
forkRuns = sourceRuns.slice(0, index + 1);
|
|
4823
|
+
forkedFromTurnId = input.atTurnId;
|
|
4824
|
+
} else if (sourceRuns.length > 0) {
|
|
4825
|
+
forkedFromTurnId = sourceRuns.at(-1).turnId;
|
|
4826
|
+
}
|
|
4827
|
+
const branchPoint = forkRuns.at(-1);
|
|
4828
|
+
if (branchPoint && !isTerminal(branchPoint.status)) {
|
|
4829
|
+
throw new Error(`Cannot fork from non-terminal run ${branchPoint.runId} (${branchPoint.status}).`);
|
|
4830
|
+
}
|
|
4831
|
+
if (forkRuns.length > maxRuns) {
|
|
4832
|
+
throw new Error(`Fork history exceeds the ${maxRuns}-run limit.`);
|
|
4833
|
+
}
|
|
4834
|
+
const count = database.query(`
|
|
4835
|
+
SELECT COUNT(*) AS count FROM zhivex_cli_sessions
|
|
4836
|
+
WHERE workspace_key = ?1 AND scope_key = ?2
|
|
4837
|
+
`).get(workspaceKey, scopeKey)?.count ?? 0;
|
|
4838
|
+
if (count >= maxSessions) {
|
|
4839
|
+
throw new Error(`Session limit reached for this workspace and scope (${maxSessions}).`);
|
|
4840
|
+
}
|
|
4841
|
+
const createdAt = now();
|
|
4842
|
+
assertTimestamp("now", createdAt);
|
|
4843
|
+
const title = normalizeTitle(input.title, maxMetadataBytes) ?? source.title;
|
|
4844
|
+
const forkSessionId = sessionId();
|
|
4845
|
+
const activitySequence = nextActivitySequence();
|
|
4846
|
+
database.query(`
|
|
4847
|
+
INSERT INTO zhivex_cli_sessions (
|
|
4848
|
+
session_id, workspace_key, scope_key, title, parent_session_id,
|
|
4849
|
+
forked_from_turn_id, revision, created_at, updated_at, activity_seq, metadata_bytes
|
|
4850
|
+
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, 0, ?7, ?7, ?8, ?9)
|
|
4851
|
+
`).run(forkSessionId, workspaceKey, scopeKey, title, candidateSessionId, forkedFromTurnId ?? null, createdAt, activitySequence, metadataSize({ title }));
|
|
4852
|
+
for (const [index, run] of forkRuns.entries()) {
|
|
4853
|
+
insertRun(forkSessionId, {
|
|
4854
|
+
runId: run.runId,
|
|
4855
|
+
provider: run.provider,
|
|
4856
|
+
model: run.model,
|
|
4857
|
+
...run.role ? { role: run.role } : {},
|
|
4858
|
+
status: run.status,
|
|
4859
|
+
createdAt: run.createdAt
|
|
4860
|
+
}, index, run.turnId);
|
|
4861
|
+
database.query(`
|
|
4862
|
+
UPDATE zhivex_cli_session_runs
|
|
4863
|
+
SET updated_at = ?1, completed_at = ?2
|
|
4864
|
+
WHERE session_id = ?3 AND ordinal = ?4
|
|
4865
|
+
`).run(run.updatedAt, run.completedAt ?? null, forkSessionId, index);
|
|
4866
|
+
}
|
|
4867
|
+
return hydrate(requireSessionRow(forkSessionId));
|
|
4868
|
+
});
|
|
4869
|
+
},
|
|
4870
|
+
async archive(candidateSessionId, input = {}) {
|
|
4871
|
+
return store.update(candidateSessionId, {
|
|
4872
|
+
archived: true,
|
|
4873
|
+
...input.expectedRevision === undefined ? {} : { expectedRevision: input.expectedRevision }
|
|
4874
|
+
});
|
|
4875
|
+
},
|
|
4876
|
+
async delete(candidateSessionId, input = {}) {
|
|
4877
|
+
return transaction(() => {
|
|
4878
|
+
const row = requireSessionRow(candidateSessionId);
|
|
4879
|
+
ensureRevision(row, input.expectedRevision);
|
|
4880
|
+
assertNoActiveRun(candidateSessionId, "delete");
|
|
4881
|
+
const deletedAt = now();
|
|
4882
|
+
assertTimestamp("now", deletedAt);
|
|
4883
|
+
const activitySequence = nextActivitySequence();
|
|
4884
|
+
database.query(`
|
|
4885
|
+
UPDATE zhivex_cli_sessions
|
|
4886
|
+
SET deleted_at = ?1, updated_at = ?1, activity_seq = ?2, revision = revision + 1
|
|
4887
|
+
WHERE session_id = ?3 AND revision = ?4
|
|
4888
|
+
`).run(deletedAt, activitySequence, candidateSessionId, row.revision);
|
|
4889
|
+
return hydrate(requireSessionRow(candidateSessionId, true));
|
|
4890
|
+
});
|
|
4891
|
+
},
|
|
4892
|
+
prune,
|
|
4893
|
+
close() {
|
|
4894
|
+
if (!closed) {
|
|
4895
|
+
closed = true;
|
|
4896
|
+
database.close(false);
|
|
4897
|
+
}
|
|
4898
|
+
}
|
|
4899
|
+
};
|
|
4900
|
+
return store;
|
|
4901
|
+
};
|
|
4902
|
+
var openSessionStore = openCliSessionStore;
|
|
4903
|
+
|
|
4904
|
+
// src/cli.ts
|
|
4905
|
+
var HARNESS_RESUME_METADATA_KEY = "zhivexHarnessResume";
|
|
4906
|
+
var HARNESS_RESUME_METADATA_SCHEMA_VERSION = 1;
|
|
4907
|
+
var CLI_EXIT_CODES = {
|
|
4908
|
+
success: 0,
|
|
4909
|
+
runtimeError: 1,
|
|
4910
|
+
usageError: 2,
|
|
4911
|
+
doctorFailed: 3
|
|
4912
|
+
};
|
|
4913
|
+
var COMMANDS = new Set(["run", "review", "chat", "providers", "doctor", "resume", "runs", "sessions", "help", "version"]);
|
|
4914
|
+
var RUNS_COMMANDS = new Set(["list", "inspect", "cancel", "cleanup", "export"]);
|
|
4915
|
+
var SESSIONS_COMMANDS = new Set(["list", "inspect", "rename", "fork", "archive"]);
|
|
4916
|
+
var RUN_STATUSES = new Set([
|
|
4917
|
+
"queued",
|
|
4918
|
+
"running",
|
|
4919
|
+
"completed",
|
|
4920
|
+
"suspended",
|
|
4921
|
+
"waiting_approval",
|
|
4922
|
+
"cancel_requested",
|
|
4923
|
+
"failed",
|
|
4924
|
+
"cancelled",
|
|
4925
|
+
"timed_out"
|
|
4926
|
+
]);
|
|
4927
|
+
|
|
4928
|
+
class CliUsageError extends Error {
|
|
4929
|
+
constructor(message) {
|
|
4930
|
+
super(message);
|
|
4931
|
+
this.name = "CliUsageError";
|
|
3892
4932
|
}
|
|
3893
4933
|
}
|
|
3894
4934
|
var harnessConfigInput = (config) => ({
|
|
@@ -3943,13 +4983,14 @@ var harnessConfigInput = (config) => ({
|
|
|
3943
4983
|
} : {},
|
|
3944
4984
|
...config.mcpConfigPath ? { mcpConfigPath: config.mcpConfigPath } : {}
|
|
3945
4985
|
});
|
|
3946
|
-
var createHarnessResumeMetadata = (config) => ({
|
|
4986
|
+
var createHarnessResumeMetadata = (config, routes) => ({
|
|
3947
4987
|
[HARNESS_RESUME_METADATA_KEY]: JSON.parse(JSON.stringify({
|
|
3948
4988
|
schemaVersion: HARNESS_RESUME_METADATA_SCHEMA_VERSION,
|
|
3949
|
-
config
|
|
4989
|
+
config,
|
|
4990
|
+
...routes && routes.size > 0 ? { routes: serializeHarnessModelRoutes(routes) } : {}
|
|
3950
4991
|
}))
|
|
3951
4992
|
});
|
|
3952
|
-
var
|
|
4993
|
+
var readHarnessResumeDocument = (state) => {
|
|
3953
4994
|
const value = state.metadata?.[HARNESS_RESUME_METADATA_KEY];
|
|
3954
4995
|
if (value === undefined)
|
|
3955
4996
|
return;
|
|
@@ -3960,12 +5001,39 @@ var readHarnessResumeConfig = (state) => {
|
|
|
3960
5001
|
if (document.schemaVersion !== HARNESS_RESUME_METADATA_SCHEMA_VERSION || !document.config || typeof document.config !== "object" || Array.isArray(document.config)) {
|
|
3961
5002
|
throw new Error("Persisted harness resume metadata is invalid.");
|
|
3962
5003
|
}
|
|
5004
|
+
return document;
|
|
5005
|
+
};
|
|
5006
|
+
var readHarnessResumeConfig = (state) => {
|
|
5007
|
+
const document = readHarnessResumeDocument(state);
|
|
5008
|
+
if (!document)
|
|
5009
|
+
return;
|
|
3963
5010
|
try {
|
|
3964
5011
|
return harnessConfigInput(document.config);
|
|
3965
5012
|
} catch {
|
|
3966
5013
|
throw new Error("Persisted harness resume metadata is invalid.");
|
|
3967
5014
|
}
|
|
3968
5015
|
};
|
|
5016
|
+
var readHarnessResumeRoutes = (state) => {
|
|
5017
|
+
const document = readHarnessResumeDocument(state);
|
|
5018
|
+
if (!document?.routes)
|
|
5019
|
+
return resolveHarnessModelRoutes();
|
|
5020
|
+
if (typeof document.routes !== "object" || Array.isArray(document.routes)) {
|
|
5021
|
+
throw new Error("Persisted harness routing metadata is invalid.");
|
|
5022
|
+
}
|
|
5023
|
+
try {
|
|
5024
|
+
const values = Object.entries(document.routes).map(([profile, target]) => {
|
|
5025
|
+
if (!target || typeof target !== "object" || Array.isArray(target))
|
|
5026
|
+
throw new Error("invalid");
|
|
5027
|
+
const route = target;
|
|
5028
|
+
if (typeof route.provider !== "string" || typeof route.model !== "string")
|
|
5029
|
+
throw new Error("invalid");
|
|
5030
|
+
return `${profile}=${route.provider}:${route.model}`;
|
|
5031
|
+
});
|
|
5032
|
+
return resolveHarnessModelRoutes(values);
|
|
5033
|
+
} catch {
|
|
5034
|
+
throw new Error("Persisted harness routing metadata is invalid.");
|
|
5035
|
+
}
|
|
5036
|
+
};
|
|
3969
5037
|
var shellArgument = (value) => `'${value.replaceAll("'", `'\\''`)}'`;
|
|
3970
5038
|
var resumeCommand = (runId, config, approve = true) => [
|
|
3971
5039
|
"zhivex-harness",
|
|
@@ -3994,7 +5062,16 @@ var parseCliArgs = (argv) => {
|
|
|
3994
5062
|
let command = "run";
|
|
3995
5063
|
let commandWasExplicit = false;
|
|
3996
5064
|
const positional = [];
|
|
3997
|
-
const options = {
|
|
5065
|
+
const options = {
|
|
5066
|
+
command,
|
|
5067
|
+
cascade: false,
|
|
5068
|
+
final: false,
|
|
5069
|
+
yes: false,
|
|
5070
|
+
json: false,
|
|
5071
|
+
jsonl: false,
|
|
5072
|
+
continueSession: false,
|
|
5073
|
+
implicitCommand: true
|
|
5074
|
+
};
|
|
3998
5075
|
let positionalOnly = false;
|
|
3999
5076
|
for (let index = 0;index < argv.length; index += 1) {
|
|
4000
5077
|
const argument = argv[index];
|
|
@@ -4028,6 +5105,21 @@ var parseCliArgs = (argv) => {
|
|
|
4028
5105
|
options.model = optionValue(argv, index, argument);
|
|
4029
5106
|
index += 1;
|
|
4030
5107
|
break;
|
|
5108
|
+
case "--route": {
|
|
5109
|
+
const value = optionValue(argv, index, argument);
|
|
5110
|
+
try {
|
|
5111
|
+
parseHarnessModelRoute(value);
|
|
5112
|
+
} catch (error) {
|
|
5113
|
+
throw new CliUsageError(error instanceof Error ? error.message : String(error));
|
|
5114
|
+
}
|
|
5115
|
+
options.routes ??= [];
|
|
5116
|
+
options.routes.push(value);
|
|
5117
|
+
if (options.routes.length > HARNESS_SUBAGENT_PROFILES.length) {
|
|
5118
|
+
throw new CliUsageError(`--route cannot be repeated more than ${HARNESS_SUBAGENT_PROFILES.length} times.`);
|
|
5119
|
+
}
|
|
5120
|
+
index += 1;
|
|
5121
|
+
break;
|
|
5122
|
+
}
|
|
4031
5123
|
case "--workspace":
|
|
4032
5124
|
options.workspace = optionValue(argv, index, argument);
|
|
4033
5125
|
index += 1;
|
|
@@ -4251,6 +5343,16 @@ var parseCliArgs = (argv) => {
|
|
|
4251
5343
|
case "--json":
|
|
4252
5344
|
options.json = true;
|
|
4253
5345
|
break;
|
|
5346
|
+
case "--jsonl":
|
|
5347
|
+
options.jsonl = true;
|
|
5348
|
+
break;
|
|
5349
|
+
case "--session":
|
|
5350
|
+
options.sessionId = optionValue(argv, index, argument);
|
|
5351
|
+
index += 1;
|
|
5352
|
+
break;
|
|
5353
|
+
case "--continue":
|
|
5354
|
+
options.continueSession = true;
|
|
5355
|
+
break;
|
|
4254
5356
|
case "--status": {
|
|
4255
5357
|
const value = optionValue(argv, index, argument);
|
|
4256
5358
|
if (!RUN_STATUSES.has(value)) {
|
|
@@ -4312,6 +5414,16 @@ var parseCliArgs = (argv) => {
|
|
|
4312
5414
|
positional.push(argument);
|
|
4313
5415
|
}
|
|
4314
5416
|
}
|
|
5417
|
+
options.implicitCommand = !commandWasExplicit;
|
|
5418
|
+
if (options.json && options.jsonl) {
|
|
5419
|
+
throw new CliUsageError("You cannot combine --json and --jsonl.");
|
|
5420
|
+
}
|
|
5421
|
+
if (options.sessionId && options.continueSession) {
|
|
5422
|
+
throw new CliUsageError("You cannot combine --session and --continue.");
|
|
5423
|
+
}
|
|
5424
|
+
if (options.jsonl && options.command !== "run" && options.command !== "resume") {
|
|
5425
|
+
throw new CliUsageError("--jsonl is supported by run and resume.");
|
|
5426
|
+
}
|
|
4315
5427
|
if (options.command === "resume") {
|
|
4316
5428
|
const runId = positional.shift();
|
|
4317
5429
|
if (runId) {
|
|
@@ -4339,6 +5451,28 @@ var parseCliArgs = (argv) => {
|
|
|
4339
5451
|
if (positional.length > 0) {
|
|
4340
5452
|
throw new CliUsageError(`runs ${runsCommand} received unexpected positional arguments.`);
|
|
4341
5453
|
}
|
|
5454
|
+
} else if (options.command === "sessions") {
|
|
5455
|
+
const sessionsCommand = positional.shift();
|
|
5456
|
+
if (!sessionsCommand || !SESSIONS_COMMANDS.has(sessionsCommand)) {
|
|
5457
|
+
throw new CliUsageError("sessions requires one of: list, inspect, rename, fork, archive.");
|
|
5458
|
+
}
|
|
5459
|
+
options.sessionsCommand = sessionsCommand;
|
|
5460
|
+
if (sessionsCommand !== "list") {
|
|
5461
|
+
const sessionId2 = positional.shift();
|
|
5462
|
+
if (!sessionId2)
|
|
5463
|
+
throw new CliUsageError(`sessions ${sessionsCommand} requires a sessionId.`);
|
|
5464
|
+
options.sessionId = sessionId2;
|
|
5465
|
+
}
|
|
5466
|
+
if (sessionsCommand === "rename") {
|
|
5467
|
+
const title = positional.join(" ").trim();
|
|
5468
|
+
if (!title)
|
|
5469
|
+
throw new CliUsageError("sessions rename requires a title.");
|
|
5470
|
+
options.sessionTitle = title;
|
|
5471
|
+
positional.length = 0;
|
|
5472
|
+
}
|
|
5473
|
+
if (positional.length > 0) {
|
|
5474
|
+
throw new CliUsageError(`sessions ${sessionsCommand} received unexpected positional arguments.`);
|
|
5475
|
+
}
|
|
4342
5476
|
} else if (options.command === "run" || options.command === "review") {
|
|
4343
5477
|
const prompt = positional.join(" ").trim();
|
|
4344
5478
|
if (prompt) {
|
|
@@ -4352,21 +5486,24 @@ var parseCliArgs = (argv) => {
|
|
|
4352
5486
|
var help = `Zhivex Harness v${HARNESS_VERSION}
|
|
4353
5487
|
|
|
4354
5488
|
Usage:
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
5489
|
+
zhx Start the interactive console
|
|
5490
|
+
zhx run [options] "task"
|
|
5491
|
+
zhx review [options] "review task"
|
|
5492
|
+
zhx chat [options] [--continue|--session <id>]
|
|
5493
|
+
zhx providers [--json]
|
|
5494
|
+
zhx doctor [options] [--json]
|
|
5495
|
+
zhx resume [options] <runId> --approve|--deny
|
|
5496
|
+
zhx runs list [--status <status>] [--limit <n>] [--json]
|
|
5497
|
+
zhx sessions list|inspect|rename|fork|archive
|
|
5498
|
+
|
|
5499
|
+
The long command zhivex-harness remains supported for compatibility.
|
|
4366
5500
|
|
|
4367
5501
|
Options:
|
|
4368
|
-
--provider
|
|
5502
|
+
--provider <${PROVIDERS.join("|")}> Provider (default: openai)
|
|
4369
5503
|
--model <id> Override the default model
|
|
5504
|
+
--route <role=provider[:model]> Route a subagent role; repeatable
|
|
5505
|
+
--session <id> Open a durable interactive session
|
|
5506
|
+
--continue Open the latest durable interactive session
|
|
4370
5507
|
--workspace <path> Target workspace (default: cwd)
|
|
4371
5508
|
--state-dir <path> Durable run-state directory
|
|
4372
5509
|
--mcp-config <path> Declarative governed MCP JSON configuration
|
|
@@ -4408,6 +5545,7 @@ Options:
|
|
|
4408
5545
|
--max-parallel-reviews <1-4> Review group concurrency ceiling (default: 2)
|
|
4409
5546
|
--yes Automatically approve writes and checks
|
|
4410
5547
|
--json Emit structured final output
|
|
5548
|
+
--jsonl Stream redacted JSON Lines, then the final result
|
|
4411
5549
|
-h, --help Show this help
|
|
4412
5550
|
-v, --version Show the version
|
|
4413
5551
|
|
|
@@ -4420,7 +5558,8 @@ Exit codes:
|
|
|
4420
5558
|
Credentials:
|
|
4421
5559
|
OpenAI: OPENAI_API_KEY
|
|
4422
5560
|
Meta: MODEL_API_KEY
|
|
4423
|
-
Qwen: DASHSCOPE_API_KEY or QWEN_API_KEY
|
|
5561
|
+
Qwen: DASHSCOPE_API_KEY or QWEN_API_KEY
|
|
5562
|
+
Gemini: GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY`;
|
|
4424
5563
|
var summarizeApproval = (approval) => {
|
|
4425
5564
|
const mustShowCompleteProposal = approval.name === "apply_patch";
|
|
4426
5565
|
const argumentsText = !mustShowCompleteProposal && approval.arguments.length > 1200 ? `${approval.arguments.slice(0, 1200)}\u2026` : approval.arguments;
|
|
@@ -4524,16 +5663,23 @@ var runResultDocument = (result, harness) => ({
|
|
|
4524
5663
|
},
|
|
4525
5664
|
stateDirectory: harness.config.stateDirectory
|
|
4526
5665
|
});
|
|
4527
|
-
var printTerminalResult = (result, harness,
|
|
4528
|
-
|
|
4529
|
-
|
|
5666
|
+
var printTerminalResult = (result, harness, output, tracker) => {
|
|
5667
|
+
const document = runResultDocument(result, harness);
|
|
5668
|
+
if (output.jsonl) {
|
|
5669
|
+
tracker.sequence = (tracker.sequence ?? 0) + 1;
|
|
5670
|
+
process.stdout.write(`${serializeStreamResult(document, tracker.sequence)}
|
|
4530
5671
|
`);
|
|
4531
5672
|
return;
|
|
4532
5673
|
}
|
|
4533
|
-
if (
|
|
5674
|
+
if (output.json) {
|
|
5675
|
+
process.stdout.write(`${JSON.stringify(document, null, 2)}
|
|
5676
|
+
`);
|
|
5677
|
+
return;
|
|
5678
|
+
}
|
|
5679
|
+
if (!tracker.streamedText && result.outputText) {
|
|
4534
5680
|
process.stdout.write(result.outputText);
|
|
4535
5681
|
}
|
|
4536
|
-
if (result.outputText || streamedText) {
|
|
5682
|
+
if (result.outputText || tracker.streamedText) {
|
|
4537
5683
|
process.stdout.write(`
|
|
4538
5684
|
`);
|
|
4539
5685
|
}
|
|
@@ -4551,8 +5697,14 @@ ${summarizeApproval(approval)}
|
|
|
4551
5697
|
`);
|
|
4552
5698
|
}
|
|
4553
5699
|
};
|
|
4554
|
-
var streamSink = (
|
|
4555
|
-
if (
|
|
5700
|
+
var streamSink = (output, tracker) => async (event) => {
|
|
5701
|
+
if (output.jsonl) {
|
|
5702
|
+
tracker.sequence = (tracker.sequence ?? 0) + 1;
|
|
5703
|
+
process.stdout.write(`${serializeStreamEvent(event, tracker.sequence)}
|
|
5704
|
+
`);
|
|
5705
|
+
return;
|
|
5706
|
+
}
|
|
5707
|
+
if (!output.json && event.type === "text-delta") {
|
|
4556
5708
|
tracker.streamedText = true;
|
|
4557
5709
|
process.stdout.write(event.textDelta);
|
|
4558
5710
|
}
|
|
@@ -4570,23 +5722,57 @@ subagent finish \xB7 ${event.childRun.agentId ?? event.childRun.toolName} \xB7 $
|
|
|
4570
5722
|
`);
|
|
4571
5723
|
}
|
|
4572
5724
|
};
|
|
5725
|
+
var resolvedRouting = (options) => {
|
|
5726
|
+
return resolveHarnessModelRoutes(options.routes);
|
|
5727
|
+
};
|
|
5728
|
+
var assertRoutePricingIsSafe = (routes, costBudget) => {
|
|
5729
|
+
if (routes.size > 0 && costBudget !== undefined) {
|
|
5730
|
+
throw new CliUsageError("Cost budgets cannot be combined with model routes until pricing is configured per role.");
|
|
5731
|
+
}
|
|
5732
|
+
};
|
|
5733
|
+
var withTemporaryHarnessProfiles = async (profiles, configure, operation) => {
|
|
5734
|
+
await configure(profiles);
|
|
5735
|
+
try {
|
|
5736
|
+
return await operation();
|
|
5737
|
+
} finally {
|
|
5738
|
+
await configure([]);
|
|
5739
|
+
}
|
|
5740
|
+
};
|
|
5741
|
+
var createConfiguredHarness = async (options, extraProfiles = [], persistedRoutes) => {
|
|
5742
|
+
const routes = persistedRoutes ?? resolvedRouting(options);
|
|
5743
|
+
const resolvedConfig = resolveHarnessConfig(options);
|
|
5744
|
+
assertRoutePricingIsSafe(routes, resolvedConfig.costBudget);
|
|
5745
|
+
const profiles = [...new Set([
|
|
5746
|
+
...options.subagentProfiles ?? [],
|
|
5747
|
+
...extraProfiles,
|
|
5748
|
+
...routes.keys()
|
|
5749
|
+
])];
|
|
5750
|
+
const quietTelemetry = options.json || options.jsonl;
|
|
5751
|
+
const harness = await createHarness({
|
|
5752
|
+
...options,
|
|
5753
|
+
subagentProfiles: profiles,
|
|
5754
|
+
subagentModels: createHarnessRouteModels(routes),
|
|
5755
|
+
onTelemetryEvent: orchestrationObserver(quietTelemetry)
|
|
5756
|
+
});
|
|
5757
|
+
return { harness, routes };
|
|
5758
|
+
};
|
|
4573
5759
|
var runOnce = async (options) => {
|
|
4574
5760
|
if (!options.prompt) {
|
|
4575
5761
|
throw new CliUsageError('Missing task. Example: zhivex-harness run "fix the tests".');
|
|
4576
5762
|
}
|
|
4577
|
-
const harness = await
|
|
5763
|
+
const { harness, routes } = await createConfiguredHarness(options);
|
|
4578
5764
|
try {
|
|
4579
5765
|
const tracker = { streamedText: false };
|
|
4580
5766
|
const result = await runHarness(harness, {
|
|
4581
5767
|
prompt: options.prompt,
|
|
4582
5768
|
scope: harness.config.scope,
|
|
4583
|
-
metadata: createHarnessResumeMetadata(harness.config),
|
|
5769
|
+
metadata: createHarnessResumeMetadata(harness.config, routes),
|
|
4584
5770
|
...options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}
|
|
4585
5771
|
}, {
|
|
4586
|
-
onEvent: streamSink(options
|
|
5772
|
+
onEvent: streamSink(options, tracker),
|
|
4587
5773
|
resolveApprovals: terminalApprovalResolver(options.yes)
|
|
4588
5774
|
});
|
|
4589
|
-
printTerminalResult(result, harness, options
|
|
5775
|
+
printTerminalResult(result, harness, options, tracker);
|
|
4590
5776
|
if (result.status === "failed" || result.status === "timed_out") {
|
|
4591
5777
|
process.exitCode = CLI_EXIT_CODES.runtimeError;
|
|
4592
5778
|
}
|
|
@@ -4603,11 +5789,7 @@ var reviewOnce = async (options) => {
|
|
|
4603
5789
|
...options.subagentProfiles ?? [],
|
|
4604
5790
|
...requestedReviewers
|
|
4605
5791
|
])];
|
|
4606
|
-
const harness = await
|
|
4607
|
-
...options,
|
|
4608
|
-
subagentProfiles: enabledProfiles,
|
|
4609
|
-
onTelemetryEvent: orchestrationObserver(options.json)
|
|
4610
|
-
});
|
|
5792
|
+
const { harness } = await createConfiguredHarness(options, enabledProfiles);
|
|
4611
5793
|
try {
|
|
4612
5794
|
const result = await runHarnessReviewGroup(harness, { prompt: options.prompt, scope: harness.config.scope }, requestedReviewers);
|
|
4613
5795
|
const document = {
|
|
@@ -4678,22 +5860,31 @@ ${summarizeApproval(approval)}
|
|
|
4678
5860
|
`);
|
|
4679
5861
|
}
|
|
4680
5862
|
const persistedResumeInput = readHarnessResumeConfig(state);
|
|
5863
|
+
const persistedRoutes = readHarnessResumeRoutes(state);
|
|
4681
5864
|
let persistedResumeOptions = {};
|
|
5865
|
+
let resumeCostBudget = initialConfig.costBudget;
|
|
4682
5866
|
if (persistedResumeInput) {
|
|
4683
5867
|
const persistedConfig = resolveHarnessConfig(persistedResumeInput);
|
|
4684
5868
|
if (persistedConfig.workspace !== initialConfig.workspace || persistedConfig.stateDirectory !== initialConfig.stateDirectory || persistedConfig.storeBackend !== initialConfig.storeBackend || JSON.stringify(persistedConfig.scope) !== JSON.stringify(initialConfig.scope)) {
|
|
4685
5869
|
throw new Error("Resume locator options do not match the persisted workspace, state directory, store, or scope.");
|
|
4686
5870
|
}
|
|
4687
5871
|
persistedResumeOptions = harnessConfigInput(persistedConfig);
|
|
5872
|
+
resumeCostBudget = persistedConfig.costBudget;
|
|
4688
5873
|
}
|
|
5874
|
+
assertRoutePricingIsSafe(persistedRoutes, resumeCostBudget);
|
|
4689
5875
|
const harness = await createHarness({
|
|
4690
5876
|
...persistedResumeOptions,
|
|
4691
5877
|
...options,
|
|
4692
5878
|
provider: state.provider,
|
|
4693
5879
|
model: state.modelId,
|
|
5880
|
+
subagentProfiles: [...new Set([
|
|
5881
|
+
...persistedResumeOptions.subagentProfiles ?? [],
|
|
5882
|
+
...persistedRoutes.keys()
|
|
5883
|
+
])],
|
|
5884
|
+
subagentModels: createHarnessRouteModels(persistedRoutes),
|
|
4694
5885
|
store: persistence.store,
|
|
4695
5886
|
memory: persistence.memory,
|
|
4696
|
-
onTelemetryEvent: orchestrationObserver(options.json)
|
|
5887
|
+
onTelemetryEvent: orchestrationObserver(options.json || options.jsonl)
|
|
4697
5888
|
});
|
|
4698
5889
|
const approve = options.approve;
|
|
4699
5890
|
const tracker = { streamedText: false };
|
|
@@ -4701,10 +5892,11 @@ ${summarizeApproval(approval)}
|
|
|
4701
5892
|
state,
|
|
4702
5893
|
approvals: approvalResponses(state.pendingApprovals, approve, approve ? "Approved by resume --approve." : "Denied by resume --deny.")
|
|
4703
5894
|
}, {
|
|
4704
|
-
onEvent: streamSink(options
|
|
5895
|
+
onEvent: streamSink(options, tracker),
|
|
4705
5896
|
resolveApprovals: async (approvals) => approvalResponses(approvals, approve, approve ? "Approved by resume --approve." : "Denied by resume --deny.")
|
|
4706
5897
|
});
|
|
4707
|
-
|
|
5898
|
+
await updateIndexedSessionRun(harness.config, result.state.runId, result.status);
|
|
5899
|
+
printTerminalResult(result, harness, options, tracker);
|
|
4708
5900
|
if (result.status === "failed" || result.status === "timed_out") {
|
|
4709
5901
|
process.exitCode = CLI_EXIT_CODES.runtimeError;
|
|
4710
5902
|
}
|
|
@@ -4716,11 +5908,87 @@ var chat = async (options) => {
|
|
|
4716
5908
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
4717
5909
|
throw new Error("Chat mode requires an interactive terminal. Use run for automation.");
|
|
4718
5910
|
}
|
|
4719
|
-
const
|
|
5911
|
+
const baseConfig = resolveHarnessConfig(options);
|
|
5912
|
+
const sessionStore = await openSessionStoreForConfig(baseConfig);
|
|
4720
5913
|
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
|
5914
|
+
let runtimeOptions = { ...options };
|
|
5915
|
+
let routes = resolvedRouting(options);
|
|
5916
|
+
const selectedSession = options.sessionId ? await sessionStore.get(options.sessionId) : options.continueSession ? await sessionStore.latest() : undefined;
|
|
5917
|
+
if (options.sessionId && !selectedSession) {
|
|
5918
|
+
sessionStore.close();
|
|
5919
|
+
readline.close();
|
|
5920
|
+
throw new Error(`Session ${options.sessionId} was not found.`);
|
|
5921
|
+
}
|
|
5922
|
+
let session = selectedSession ?? await sessionStore.create();
|
|
4721
5923
|
let messages = [];
|
|
5924
|
+
const latestState = async (selected) => {
|
|
5925
|
+
const latest = selected.runs.at(-1);
|
|
5926
|
+
if (!latest)
|
|
5927
|
+
return;
|
|
5928
|
+
const persistence = await openHarnessPersistence(baseConfig);
|
|
5929
|
+
try {
|
|
5930
|
+
const state = await persistence.store.load(latest.runId, baseConfig.scope);
|
|
5931
|
+
if (!state)
|
|
5932
|
+
throw new Error(`Run ${latest.runId} referenced by session ${selected.sessionId} was not found.`);
|
|
5933
|
+
return state;
|
|
5934
|
+
} finally {
|
|
5935
|
+
persistence.close();
|
|
5936
|
+
}
|
|
5937
|
+
};
|
|
5938
|
+
let harness;
|
|
5939
|
+
try {
|
|
5940
|
+
const restored = await latestState(session);
|
|
5941
|
+
if (restored) {
|
|
5942
|
+
runtimeOptions = { ...runtimeOptions, provider: restored.provider, model: restored.modelId };
|
|
5943
|
+
routes = readHarnessResumeRoutes(restored);
|
|
5944
|
+
messages = restored.messages;
|
|
5945
|
+
}
|
|
5946
|
+
harness = (await createConfiguredHarness(runtimeOptions, [], routes)).harness;
|
|
5947
|
+
} catch (error) {
|
|
5948
|
+
readline.close();
|
|
5949
|
+
sessionStore.close();
|
|
5950
|
+
throw error;
|
|
5951
|
+
}
|
|
5952
|
+
const refreshSession = async () => {
|
|
5953
|
+
const refreshed = await sessionStore.get(session.sessionId);
|
|
5954
|
+
if (!refreshed)
|
|
5955
|
+
throw new Error(`Session ${session.sessionId} was not found.`);
|
|
5956
|
+
session = refreshed;
|
|
5957
|
+
return session;
|
|
5958
|
+
};
|
|
5959
|
+
const hasActiveTurn = async () => {
|
|
5960
|
+
const latest = (await refreshSession()).runs.at(-1);
|
|
5961
|
+
return latest && !["completed", "failed", "cancelled", "timed_out"].includes(latest.status) ? latest : undefined;
|
|
5962
|
+
};
|
|
5963
|
+
const replaceHarness = async (nextOptions, nextRoutes, extraProfiles = []) => {
|
|
5964
|
+
const created = await createConfiguredHarness(nextOptions, extraProfiles, nextRoutes);
|
|
5965
|
+
harness.close();
|
|
5966
|
+
harness = created.harness;
|
|
5967
|
+
runtimeOptions = nextOptions;
|
|
5968
|
+
routes = new Map(nextRoutes);
|
|
5969
|
+
};
|
|
5970
|
+
const withTemporaryProfiles = async (profiles, operation) => {
|
|
5971
|
+
const normalOptions = runtimeOptions;
|
|
5972
|
+
const normalRoutes = new Map(routes);
|
|
5973
|
+
return withTemporaryHarnessProfiles(profiles, (temporaryProfiles) => replaceHarness(normalOptions, normalRoutes, temporaryProfiles), operation);
|
|
5974
|
+
};
|
|
5975
|
+
const restoreSession = async (selected) => {
|
|
5976
|
+
const state = await latestState(selected);
|
|
5977
|
+
const nextOptions = state ? { ...runtimeOptions, provider: state.provider, model: state.modelId } : runtimeOptions;
|
|
5978
|
+
const nextRoutes = state ? readHarnessResumeRoutes(state) : resolveHarnessModelRoutes();
|
|
5979
|
+
await replaceHarness(nextOptions, nextRoutes);
|
|
5980
|
+
session = selected;
|
|
5981
|
+
messages = state?.messages ?? [];
|
|
5982
|
+
};
|
|
5983
|
+
const statusLine = async () => {
|
|
5984
|
+
const current = await refreshSession();
|
|
5985
|
+
const latest = current.runs.at(-1);
|
|
5986
|
+
const routeText = routes.size === 0 ? "default" : [...routes.values()].map((route) => `${route.profile}=${route.provider}:${route.model}`).join(", ");
|
|
5987
|
+
return `session ${current.sessionId} \xB7 ${harness.config.provider}/${harness.config.model}` + ` \xB7 ${messages.length} messages \xB7 routes ${routeText}` + `${latest ? ` \xB7 last ${latest.runId} (${latest.status})` : ""}`;
|
|
5988
|
+
};
|
|
4722
5989
|
process.stderr.write(`Zhivex Harness ${HARNESS_VERSION} \xB7 ${harness.config.provider}/${harness.config.model}
|
|
4723
|
-
` + `
|
|
5990
|
+
` + `session ${session.sessionId}
|
|
5991
|
+
` + `Type /help for console commands.
|
|
4724
5992
|
`);
|
|
4725
5993
|
try {
|
|
4726
5994
|
for (;; ) {
|
|
@@ -4732,39 +6000,253 @@ var chat = async (options) => {
|
|
|
4732
6000
|
if (prompt === "/exit" || prompt === "/quit") {
|
|
4733
6001
|
break;
|
|
4734
6002
|
}
|
|
6003
|
+
if (prompt === "/help") {
|
|
6004
|
+
process.stderr.write(`/provider [id] \xB7 /model [id] \xB7 /route [role=provider[:model]] \xB7 /status
|
|
6005
|
+
` + `/diff \xB7 /review <task> \xB7 /resume <last|sessionId> \xB7 /compact
|
|
6006
|
+
` + `/new [title] \xB7 /rename <title> \xB7 /clear \xB7 /exit
|
|
6007
|
+
`);
|
|
6008
|
+
continue;
|
|
6009
|
+
}
|
|
4735
6010
|
if (prompt === "/clear") {
|
|
4736
6011
|
messages = [];
|
|
4737
6012
|
process.stderr.write(`Context cleared.
|
|
4738
6013
|
`);
|
|
4739
6014
|
continue;
|
|
4740
6015
|
}
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
6016
|
+
if (prompt === "/status") {
|
|
6017
|
+
process.stderr.write(`${await statusLine()}
|
|
6018
|
+
`);
|
|
6019
|
+
continue;
|
|
6020
|
+
}
|
|
6021
|
+
if (prompt === "/diff") {
|
|
6022
|
+
const diff = await harness.workspace.gitDiff();
|
|
6023
|
+
const output = `${diff.status.stdout}${diff.diff.stdout}${diff.staged.stdout}`;
|
|
6024
|
+
process.stdout.write(output || `No workspace changes.
|
|
6025
|
+
`);
|
|
6026
|
+
continue;
|
|
6027
|
+
}
|
|
6028
|
+
if (prompt === "/compact") {
|
|
6029
|
+
messages = compactHarnessMessages(messages);
|
|
6030
|
+
process.stderr.write(`Context compacted to ${messages.length} redacted message(s).
|
|
6031
|
+
`);
|
|
6032
|
+
continue;
|
|
6033
|
+
}
|
|
6034
|
+
if (prompt === "/provider" || prompt.startsWith("/provider ")) {
|
|
6035
|
+
const value = prompt.slice("/provider".length).trim();
|
|
6036
|
+
if (!value) {
|
|
6037
|
+
process.stderr.write(`${providerAvailability().map((provider2) => `${provider2.id}${provider2.id === harness.config.provider ? "*" : ""}`).join(" ")}
|
|
6038
|
+
`);
|
|
6039
|
+
continue;
|
|
6040
|
+
}
|
|
6041
|
+
const active2 = await hasActiveTurn();
|
|
6042
|
+
if (active2) {
|
|
6043
|
+
process.stderr.write(`Cannot switch provider while run ${active2.runId} is ${active2.status}.
|
|
6044
|
+
`);
|
|
6045
|
+
continue;
|
|
6046
|
+
}
|
|
6047
|
+
const provider = parseProvider(value);
|
|
6048
|
+
const model = DEFAULT_PROVIDER_REGISTRY.descriptor(provider).defaultModel;
|
|
6049
|
+
const portableMessages = compactHarnessMessages(messages);
|
|
6050
|
+
await replaceHarness({ ...runtimeOptions, provider, model }, routes);
|
|
6051
|
+
messages = portableMessages;
|
|
6052
|
+
process.stderr.write(`Next turn: ${provider}/${model}; context was compacted for a safe handoff.
|
|
6053
|
+
`);
|
|
6054
|
+
continue;
|
|
6055
|
+
}
|
|
6056
|
+
if (prompt === "/model" || prompt.startsWith("/model ")) {
|
|
6057
|
+
const value = prompt.slice("/model".length).trim();
|
|
6058
|
+
if (!value) {
|
|
6059
|
+
process.stderr.write(`${harness.config.provider}/${harness.config.model}
|
|
6060
|
+
`);
|
|
6061
|
+
continue;
|
|
6062
|
+
}
|
|
6063
|
+
const active2 = await hasActiveTurn();
|
|
6064
|
+
if (active2) {
|
|
6065
|
+
process.stderr.write(`Cannot switch model while run ${active2.runId} is ${active2.status}.
|
|
6066
|
+
`);
|
|
6067
|
+
continue;
|
|
6068
|
+
}
|
|
6069
|
+
const portableMessages = compactHarnessMessages(messages);
|
|
6070
|
+
await replaceHarness({ ...runtimeOptions, model: value }, routes);
|
|
6071
|
+
messages = portableMessages;
|
|
6072
|
+
process.stderr.write(`Next turn: ${harness.config.provider}/${value}; context was compacted.
|
|
6073
|
+
`);
|
|
6074
|
+
continue;
|
|
6075
|
+
}
|
|
6076
|
+
if (prompt === "/route" || prompt.startsWith("/route ")) {
|
|
6077
|
+
const value = prompt.slice("/route".length).trim();
|
|
6078
|
+
if (!value) {
|
|
6079
|
+
process.stderr.write(`${JSON.stringify(serializeHarnessModelRoutes(routes))}
|
|
6080
|
+
`);
|
|
6081
|
+
continue;
|
|
6082
|
+
}
|
|
6083
|
+
const active2 = await hasActiveTurn();
|
|
6084
|
+
if (active2) {
|
|
6085
|
+
process.stderr.write(`Cannot change routes while run ${active2.runId} is ${active2.status}.
|
|
6086
|
+
`);
|
|
6087
|
+
continue;
|
|
6088
|
+
}
|
|
6089
|
+
const nextRoutes = new Map(routes);
|
|
6090
|
+
if (value === "clear") {
|
|
6091
|
+
nextRoutes.clear();
|
|
6092
|
+
} else if (value.startsWith("clear ")) {
|
|
6093
|
+
const profile = value.slice("clear ".length).trim();
|
|
6094
|
+
if (!HARNESS_SUBAGENT_PROFILES.includes(profile)) {
|
|
6095
|
+
process.stderr.write(`Unknown subagent profile: ${profile}.
|
|
6096
|
+
`);
|
|
6097
|
+
continue;
|
|
6098
|
+
}
|
|
6099
|
+
nextRoutes.delete(profile);
|
|
6100
|
+
} else {
|
|
6101
|
+
const route = parseHarnessModelRoute(value);
|
|
6102
|
+
nextRoutes.set(route.profile, route);
|
|
6103
|
+
}
|
|
6104
|
+
await replaceHarness(runtimeOptions, nextRoutes);
|
|
6105
|
+
process.stderr.write(`Routes: ${JSON.stringify(serializeHarnessModelRoutes(routes))}
|
|
6106
|
+
`);
|
|
6107
|
+
continue;
|
|
6108
|
+
}
|
|
6109
|
+
if (prompt === "/new" || prompt.startsWith("/new ")) {
|
|
6110
|
+
const active2 = await hasActiveTurn();
|
|
6111
|
+
if (active2) {
|
|
6112
|
+
process.stderr.write(`Cannot leave session while run ${active2.runId} is ${active2.status}.
|
|
6113
|
+
`);
|
|
6114
|
+
continue;
|
|
6115
|
+
}
|
|
6116
|
+
const title = prompt.slice("/new".length).trim();
|
|
6117
|
+
const created = await sessionStore.create(title ? { title } : undefined);
|
|
6118
|
+
await restoreSession(created);
|
|
6119
|
+
process.stderr.write(`Created session ${session.sessionId}.
|
|
6120
|
+
`);
|
|
6121
|
+
continue;
|
|
6122
|
+
}
|
|
6123
|
+
if (prompt === "/rename" || prompt.startsWith("/rename ")) {
|
|
6124
|
+
const title = prompt.slice("/rename".length).trim();
|
|
6125
|
+
if (!title) {
|
|
6126
|
+
process.stderr.write(`Usage: /rename <title>
|
|
6127
|
+
`);
|
|
6128
|
+
continue;
|
|
6129
|
+
}
|
|
6130
|
+
session = await sessionStore.rename(session.sessionId, title);
|
|
6131
|
+
process.stderr.write(`Renamed session to ${session.title}.
|
|
6132
|
+
`);
|
|
6133
|
+
continue;
|
|
6134
|
+
}
|
|
6135
|
+
if (prompt === "/resume" || prompt.startsWith("/resume ")) {
|
|
6136
|
+
const selector = prompt.slice("/resume".length).trim() || "last";
|
|
6137
|
+
const selected = selector === "last" ? await sessionStore.latest({ includeArchived: true }) : await sessionStore.get(selector);
|
|
6138
|
+
if (!selected) {
|
|
6139
|
+
process.stderr.write(`Session ${selector} was not found.
|
|
6140
|
+
`);
|
|
6141
|
+
continue;
|
|
6142
|
+
}
|
|
6143
|
+
await restoreSession(selected);
|
|
6144
|
+
const active2 = await hasActiveTurn();
|
|
6145
|
+
process.stderr.write(active2 ? `Session ${session.sessionId} has run ${active2.runId} waiting in ${active2.status}; use zhx resume ${active2.runId} --approve|--deny.
|
|
6146
|
+
` : `Resumed session ${session.sessionId}.
|
|
6147
|
+
`);
|
|
6148
|
+
continue;
|
|
6149
|
+
}
|
|
6150
|
+
if (prompt === "/review" || prompt.startsWith("/review ")) {
|
|
6151
|
+
const reviewPrompt = prompt.slice("/review".length).trim();
|
|
6152
|
+
if (!reviewPrompt) {
|
|
6153
|
+
process.stderr.write(`Usage: /review <task>
|
|
6154
|
+
`);
|
|
6155
|
+
continue;
|
|
6156
|
+
}
|
|
6157
|
+
const active2 = await hasActiveTurn();
|
|
6158
|
+
if (active2) {
|
|
6159
|
+
process.stderr.write(`Cannot start a review while run ${active2.runId} is ${active2.status}.
|
|
6160
|
+
`);
|
|
6161
|
+
continue;
|
|
6162
|
+
}
|
|
6163
|
+
const review = await withTemporaryProfiles(["explorer", "reviewer"], () => runHarnessReviewGroup(harness, { prompt: reviewPrompt, scope: harness.config.scope }, ["explorer", "reviewer"]));
|
|
6164
|
+
for (const output of review.outputs) {
|
|
6165
|
+
process.stdout.write(`
|
|
6166
|
+
[${output.name ?? output.agentId ?? "reviewer"}] ${output.status}
|
|
6167
|
+
`);
|
|
6168
|
+
if (output.output?.outputText)
|
|
6169
|
+
process.stdout.write(`${output.output.outputText}
|
|
6170
|
+
`);
|
|
6171
|
+
}
|
|
6172
|
+
continue;
|
|
6173
|
+
}
|
|
6174
|
+
const active = await hasActiveTurn();
|
|
6175
|
+
if (active) {
|
|
6176
|
+
process.stderr.write(`Run ${active.runId} is ${active.status}; resolve it with zhx resume ${active.runId} --approve|--deny before a new turn.
|
|
6177
|
+
`);
|
|
6178
|
+
continue;
|
|
6179
|
+
}
|
|
6180
|
+
const runId = `run_${randomUUID5()}`;
|
|
6181
|
+
session = await sessionStore.appendRun(session.sessionId, {
|
|
6182
|
+
runId,
|
|
6183
|
+
provider: harness.config.provider,
|
|
6184
|
+
model: harness.config.model,
|
|
6185
|
+
status: "created"
|
|
4753
6186
|
});
|
|
6187
|
+
const turn = session.runs.at(-1);
|
|
6188
|
+
const tracker = { streamedText: false };
|
|
6189
|
+
let markedRunning = false;
|
|
6190
|
+
let result;
|
|
6191
|
+
try {
|
|
6192
|
+
result = await runHarness(harness, messages.length === 0 ? {
|
|
6193
|
+
runId,
|
|
6194
|
+
prompt,
|
|
6195
|
+
scope: harness.config.scope,
|
|
6196
|
+
metadata: {
|
|
6197
|
+
...createHarnessResumeMetadata(harness.config, routes),
|
|
6198
|
+
zhivexCliSession: {
|
|
6199
|
+
schemaVersion: 1,
|
|
6200
|
+
sessionId: session.sessionId,
|
|
6201
|
+
turnId: turn.turnId,
|
|
6202
|
+
sequence: turn.sequence
|
|
6203
|
+
}
|
|
6204
|
+
}
|
|
6205
|
+
} : {
|
|
6206
|
+
runId,
|
|
6207
|
+
messages: appendUserMessage(messages, prompt),
|
|
6208
|
+
scope: harness.config.scope,
|
|
6209
|
+
metadata: {
|
|
6210
|
+
...createHarnessResumeMetadata(harness.config, routes),
|
|
6211
|
+
zhivexCliSession: {
|
|
6212
|
+
schemaVersion: 1,
|
|
6213
|
+
sessionId: session.sessionId,
|
|
6214
|
+
turnId: turn.turnId,
|
|
6215
|
+
sequence: turn.sequence
|
|
6216
|
+
}
|
|
6217
|
+
}
|
|
6218
|
+
}, {
|
|
6219
|
+
onEvent: async (event) => {
|
|
6220
|
+
if (!markedRunning && event.type === "agent-run-start") {
|
|
6221
|
+
session = await sessionStore.updateRun(session.sessionId, runId, { status: "running" });
|
|
6222
|
+
markedRunning = true;
|
|
6223
|
+
}
|
|
6224
|
+
await streamSink({ json: false, jsonl: false }, tracker)(event);
|
|
6225
|
+
},
|
|
6226
|
+
resolveApprovals: terminalApprovalResolver(options.yes, (question) => readline.question(question))
|
|
6227
|
+
});
|
|
6228
|
+
} catch (error) {
|
|
6229
|
+
session = await sessionStore.updateRun(session.sessionId, runId, { status: "failed" });
|
|
6230
|
+
throw error;
|
|
6231
|
+
}
|
|
4754
6232
|
if (!tracker.streamedText && result.outputText) {
|
|
4755
6233
|
process.stdout.write(result.outputText);
|
|
4756
6234
|
}
|
|
4757
6235
|
process.stdout.write(`
|
|
4758
6236
|
`);
|
|
4759
6237
|
messages = result.messages;
|
|
6238
|
+
session = await sessionStore.updateRun(session.sessionId, runId, {
|
|
6239
|
+
status: sessionStatus(result.status)
|
|
6240
|
+
});
|
|
4760
6241
|
if (result.status === "waiting_approval") {
|
|
4761
|
-
process.stderr.write(`Run paused: ${result.state.runId}
|
|
6242
|
+
process.stderr.write(`Run paused: ${result.state.runId}. Resume with zhx resume ${result.state.runId} --approve|--deny.
|
|
4762
6243
|
`);
|
|
4763
6244
|
}
|
|
4764
6245
|
}
|
|
4765
6246
|
} finally {
|
|
4766
6247
|
readline.close();
|
|
4767
6248
|
harness.close();
|
|
6249
|
+
sessionStore.close();
|
|
4768
6250
|
}
|
|
4769
6251
|
};
|
|
4770
6252
|
var providersDocument = (env = process.env) => ({
|
|
@@ -4819,7 +6301,7 @@ var satisfiesBunEngine = (version, range) => {
|
|
|
4819
6301
|
var diagnostic = (id, status, message, details = {}) => ({ id, status, message, details });
|
|
4820
6302
|
var inspectWorkspace = async (workspace) => {
|
|
4821
6303
|
try {
|
|
4822
|
-
const workspaceStat = await
|
|
6304
|
+
const workspaceStat = await stat4(workspace);
|
|
4823
6305
|
if (!workspaceStat.isDirectory()) {
|
|
4824
6306
|
return diagnostic("workspace", "fail", "Workspace is not a directory.", { path: workspace });
|
|
4825
6307
|
}
|
|
@@ -4861,9 +6343,9 @@ var inspectGit = async (workspace) => {
|
|
|
4861
6343
|
}
|
|
4862
6344
|
};
|
|
4863
6345
|
var inspectScripts = async (workspace, allowedChecks) => {
|
|
4864
|
-
const packagePath =
|
|
6346
|
+
const packagePath = path9.join(workspace, "package.json");
|
|
4865
6347
|
try {
|
|
4866
|
-
const packageStat = await
|
|
6348
|
+
const packageStat = await stat4(packagePath);
|
|
4867
6349
|
if (!packageStat.isFile() || packageStat.size > 1024 * 1024) {
|
|
4868
6350
|
return diagnostic("scripts", "warn", "package.json is not a readable regular file.", {
|
|
4869
6351
|
packageJson: false,
|
|
@@ -4885,17 +6367,17 @@ var inspectScripts = async (workspace, allowedChecks) => {
|
|
|
4885
6367
|
}
|
|
4886
6368
|
};
|
|
4887
6369
|
var isInsidePath2 = (root, candidate) => {
|
|
4888
|
-
const relative =
|
|
4889
|
-
return relative === "" || !relative.startsWith(`..${
|
|
6370
|
+
const relative = path9.relative(root, candidate);
|
|
6371
|
+
return relative === "" || !relative.startsWith(`..${path9.sep}`) && relative !== ".." && !path9.isAbsolute(relative);
|
|
4890
6372
|
};
|
|
4891
6373
|
var containsSymlink = async (candidate, trustedPrefix) => {
|
|
4892
|
-
const parsed =
|
|
4893
|
-
const parts = trustedPrefix ?
|
|
4894
|
-
let current = trustedPrefix ??
|
|
6374
|
+
const parsed = path9.parse(candidate);
|
|
6375
|
+
const parts = trustedPrefix ? path9.relative(trustedPrefix, candidate).split(path9.sep).filter(Boolean) : candidate.slice(parsed.root.length).split(path9.sep).filter(Boolean).slice(1);
|
|
6376
|
+
let current = trustedPrefix ?? path9.join(parsed.root, candidate.slice(parsed.root.length).split(path9.sep).filter(Boolean)[0] ?? "");
|
|
4895
6377
|
for (const part of parts) {
|
|
4896
|
-
current =
|
|
6378
|
+
current = path9.join(current, part);
|
|
4897
6379
|
try {
|
|
4898
|
-
if ((await
|
|
6380
|
+
if ((await lstat7(current)).isSymbolicLink()) {
|
|
4899
6381
|
return true;
|
|
4900
6382
|
}
|
|
4901
6383
|
} catch (error) {
|
|
@@ -4911,12 +6393,12 @@ var nearestExistingPath = async (candidate) => {
|
|
|
4911
6393
|
let current = candidate;
|
|
4912
6394
|
for (;; ) {
|
|
4913
6395
|
try {
|
|
4914
|
-
return { path: current, stat: await
|
|
6396
|
+
return { path: current, stat: await stat4(current) };
|
|
4915
6397
|
} catch (error) {
|
|
4916
6398
|
if (error.code !== "ENOENT") {
|
|
4917
6399
|
throw error;
|
|
4918
6400
|
}
|
|
4919
|
-
const parent =
|
|
6401
|
+
const parent = path9.dirname(current);
|
|
4920
6402
|
if (parent === current) {
|
|
4921
6403
|
throw error;
|
|
4922
6404
|
}
|
|
@@ -4932,7 +6414,7 @@ var inspectStateDirectory = async (workspace, stateDirectory) => {
|
|
|
4932
6414
|
});
|
|
4933
6415
|
}
|
|
4934
6416
|
if (isInsidePath2(workspace, stateDirectory)) {
|
|
4935
|
-
const segments =
|
|
6417
|
+
const segments = path9.relative(workspace, stateDirectory).split(path9.sep).filter(Boolean);
|
|
4936
6418
|
const sensitiveSegment = segments.find(isSensitiveStateSegment);
|
|
4937
6419
|
if (sensitiveSegment) {
|
|
4938
6420
|
return diagnostic("state-directory", "fail", "State directory is inside a sensitive workspace path.", {
|
|
@@ -4979,9 +6461,9 @@ var inspectOperationsStore = async (stateDirectory, storeBackend) => {
|
|
|
4979
6461
|
migrationAvailable: true
|
|
4980
6462
|
});
|
|
4981
6463
|
}
|
|
4982
|
-
const databasePath =
|
|
6464
|
+
const databasePath = path9.join(stateDirectory, "operations.sqlite");
|
|
4983
6465
|
try {
|
|
4984
|
-
const entry = await
|
|
6466
|
+
const entry = await lstat7(databasePath);
|
|
4985
6467
|
if (entry.isSymbolicLink() || !entry.isFile()) {
|
|
4986
6468
|
return diagnostic("operations-store", "fail", "SQLite state path is not a regular file.", {
|
|
4987
6469
|
backend: storeBackend,
|
|
@@ -5269,6 +6751,95 @@ var manageRuns = async (options) => {
|
|
|
5269
6751
|
persistence.close();
|
|
5270
6752
|
}
|
|
5271
6753
|
};
|
|
6754
|
+
var openSessionStoreForConfig = (config) => openCliSessionStore({
|
|
6755
|
+
workspace: config.workspace,
|
|
6756
|
+
stateDirectory: config.stateDirectory,
|
|
6757
|
+
scope: config.scope
|
|
6758
|
+
});
|
|
6759
|
+
var printSessionDocument = (document, json) => {
|
|
6760
|
+
if (json) {
|
|
6761
|
+
process.stdout.write(`${JSON.stringify(document, null, 2)}
|
|
6762
|
+
`);
|
|
6763
|
+
return;
|
|
6764
|
+
}
|
|
6765
|
+
const record = document;
|
|
6766
|
+
if (record.kind === "session-list") {
|
|
6767
|
+
for (const session2 of record.sessions ?? []) {
|
|
6768
|
+
const latest = session2.runs.at(-1);
|
|
6769
|
+
process.stdout.write(`${session2.sessionId} ${session2.title ?? "(untitled)"} \xB7 ${session2.runs.length} runs` + `${latest ? ` \xB7 ${latest.provider}/${latest.model} \xB7 ${latest.status}` : ""}
|
|
6770
|
+
`);
|
|
6771
|
+
}
|
|
6772
|
+
return;
|
|
6773
|
+
}
|
|
6774
|
+
const session = record.session;
|
|
6775
|
+
if (session) {
|
|
6776
|
+
process.stdout.write(`${session.sessionId} \xB7 ${session.title ?? "(untitled)"} \xB7 ${session.runs.length} runs` + `${session.archivedAt ? " \xB7 archived" : ""}
|
|
6777
|
+
`);
|
|
6778
|
+
return;
|
|
6779
|
+
}
|
|
6780
|
+
process.stdout.write(`${JSON.stringify(document, null, 2)}
|
|
6781
|
+
`);
|
|
6782
|
+
};
|
|
6783
|
+
var manageSessions = async (options) => {
|
|
6784
|
+
const config = resolveHarnessConfig(options);
|
|
6785
|
+
const store = await openSessionStoreForConfig(config);
|
|
6786
|
+
try {
|
|
6787
|
+
let session;
|
|
6788
|
+
let document;
|
|
6789
|
+
switch (options.sessionsCommand) {
|
|
6790
|
+
case "list": {
|
|
6791
|
+
const summaries = await store.list(options.limit ? { limit: options.limit } : undefined);
|
|
6792
|
+
const sessions = (await Promise.all(summaries.map((summary) => store.get(summary.sessionId)))).filter((value) => Boolean(value));
|
|
6793
|
+
document = { schemaVersion: CLI_JSON_SCHEMA_VERSION, kind: "session-list", sessions };
|
|
6794
|
+
break;
|
|
6795
|
+
}
|
|
6796
|
+
case "inspect":
|
|
6797
|
+
session = await store.get(options.sessionId);
|
|
6798
|
+
if (!session)
|
|
6799
|
+
throw new Error(`Session ${options.sessionId} was not found.`);
|
|
6800
|
+
document = { schemaVersion: CLI_JSON_SCHEMA_VERSION, kind: "session", session };
|
|
6801
|
+
break;
|
|
6802
|
+
case "rename":
|
|
6803
|
+
session = await store.rename(options.sessionId, options.sessionTitle);
|
|
6804
|
+
document = { schemaVersion: CLI_JSON_SCHEMA_VERSION, kind: "session", session };
|
|
6805
|
+
break;
|
|
6806
|
+
case "fork":
|
|
6807
|
+
session = await store.fork(options.sessionId);
|
|
6808
|
+
document = { schemaVersion: CLI_JSON_SCHEMA_VERSION, kind: "session", session };
|
|
6809
|
+
break;
|
|
6810
|
+
case "archive":
|
|
6811
|
+
session = await store.archive(options.sessionId);
|
|
6812
|
+
document = { schemaVersion: CLI_JSON_SCHEMA_VERSION, kind: "session", session };
|
|
6813
|
+
break;
|
|
6814
|
+
default:
|
|
6815
|
+
throw new CliUsageError("sessions requires one of: list, inspect, rename, fork, archive.");
|
|
6816
|
+
}
|
|
6817
|
+
printSessionDocument(document, options.json);
|
|
6818
|
+
} finally {
|
|
6819
|
+
store.close();
|
|
6820
|
+
}
|
|
6821
|
+
};
|
|
6822
|
+
var sessionStatus = (status) => {
|
|
6823
|
+
if (status === "queued")
|
|
6824
|
+
return "created";
|
|
6825
|
+
if (status === "suspended")
|
|
6826
|
+
return "waiting_approval";
|
|
6827
|
+
return status;
|
|
6828
|
+
};
|
|
6829
|
+
var updateIndexedSessionRun = async (config, runId, status) => {
|
|
6830
|
+
const store = await openSessionStoreForConfig(config);
|
|
6831
|
+
try {
|
|
6832
|
+
for (const summary of await store.list({ limit: 200, includeArchived: true })) {
|
|
6833
|
+
const session = await store.get(summary.sessionId);
|
|
6834
|
+
if (session?.runs.some((run) => run.runId === runId)) {
|
|
6835
|
+
await store.updateRun(session.sessionId, runId, { status: sessionStatus(status) });
|
|
6836
|
+
return;
|
|
6837
|
+
}
|
|
6838
|
+
}
|
|
6839
|
+
} finally {
|
|
6840
|
+
store.close();
|
|
6841
|
+
}
|
|
6842
|
+
};
|
|
5272
6843
|
var cliExitCodeForError = (error) => error instanceof CliUsageError ? CLI_EXIT_CODES.usageError : CLI_EXIT_CODES.runtimeError;
|
|
5273
6844
|
var main = async (argv = process.argv.slice(2)) => {
|
|
5274
6845
|
const options = parseCliArgs(argv);
|
|
@@ -5296,7 +6867,14 @@ var main = async (argv = process.argv.slice(2)) => {
|
|
|
5296
6867
|
case "runs":
|
|
5297
6868
|
await manageRuns(options);
|
|
5298
6869
|
return;
|
|
6870
|
+
case "sessions":
|
|
6871
|
+
await manageSessions(options);
|
|
6872
|
+
return;
|
|
5299
6873
|
case "run":
|
|
6874
|
+
if (options.implicitCommand && !options.prompt && process.stdin.isTTY && process.stdout.isTTY) {
|
|
6875
|
+
await chat(options);
|
|
6876
|
+
return;
|
|
6877
|
+
}
|
|
5300
6878
|
await runOnce(options);
|
|
5301
6879
|
return;
|
|
5302
6880
|
case "review":
|
|
@@ -5312,9 +6890,11 @@ if (import.meta.main) {
|
|
|
5312
6890
|
});
|
|
5313
6891
|
}
|
|
5314
6892
|
export {
|
|
6893
|
+
withTemporaryHarnessProfiles,
|
|
5315
6894
|
summarizeApproval,
|
|
5316
6895
|
runResultDocument,
|
|
5317
6896
|
resumeCommand,
|
|
6897
|
+
readHarnessResumeRoutes,
|
|
5318
6898
|
readHarnessResumeConfig,
|
|
5319
6899
|
providersDocument,
|
|
5320
6900
|
parseCliArgs,
|