@harness-engineering/intelligence 0.8.0 → 0.9.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/dist/index.d.mts +30 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +124 -23
- package/dist/index.mjs +124 -23
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -367,6 +367,14 @@ interface AnalysisRequest {
|
|
|
367
367
|
responseSchema: z.ZodType;
|
|
368
368
|
model?: string;
|
|
369
369
|
maxTokens?: number;
|
|
370
|
+
/**
|
|
371
|
+
* Request that the backend suppress any chain-of-thought / `<think>` reasoning for THIS call.
|
|
372
|
+
* Intended for narrow structured-extraction calls where a reasoning trace adds latency but not
|
|
373
|
+
* quality. Advisory + best-effort: a provider that can honor it (e.g. Ollama's native
|
|
374
|
+
* `think:false`) does; one that cannot ignores it and answers normally. Never changes the
|
|
375
|
+
* response contract — only whether the model reasons out loud first.
|
|
376
|
+
*/
|
|
377
|
+
disableThinking?: boolean;
|
|
370
378
|
}
|
|
371
379
|
interface AnalysisResponse<T> {
|
|
372
380
|
result: T;
|
|
@@ -437,12 +445,26 @@ interface OpenAICompatibleProviderOptions {
|
|
|
437
445
|
*/
|
|
438
446
|
declare class OpenAICompatibleAnalysisProvider implements AnalysisProvider {
|
|
439
447
|
private readonly client;
|
|
448
|
+
private readonly baseUrl;
|
|
449
|
+
private readonly timeoutMs;
|
|
440
450
|
private readonly defaultModel;
|
|
441
451
|
private readonly getModel?;
|
|
442
452
|
private readonly promptSuffix;
|
|
443
453
|
private readonly jsonMode;
|
|
444
454
|
constructor(options: OpenAICompatibleProviderOptions);
|
|
445
455
|
analyze<T>(request: AnalysisRequest): Promise<AnalysisResponse<T>>;
|
|
456
|
+
/** Build the system + user message content shared by both the OpenAI-compat and native paths. */
|
|
457
|
+
private buildMessages;
|
|
458
|
+
/** Ollama's native chat route, derived from the configured `/v1` base URL. */
|
|
459
|
+
private ollamaNativeChatUrl;
|
|
460
|
+
/**
|
|
461
|
+
* Take Ollama's NATIVE `/api/chat` with `think:false` — the only way to actually suppress a
|
|
462
|
+
* Qwen3-style reasoning trace (the OpenAI-compatible `/v1` shim ignores every thinking knob).
|
|
463
|
+
* Schema is enforced via native `format`; `num_predict` bounds output. Throws on any non-2xx,
|
|
464
|
+
* missing content, or schema-parse failure so `analyze` can fall back to the OpenAI-compatible
|
|
465
|
+
* path (e.g. when the endpoint is not actually Ollama).
|
|
466
|
+
*/
|
|
467
|
+
private analyzeViaOllamaNative;
|
|
446
468
|
}
|
|
447
469
|
|
|
448
470
|
interface ClaudeCliProviderOptions {
|
|
@@ -1759,6 +1781,14 @@ interface ProbeConfig {
|
|
|
1759
1781
|
interface ProbeDeps {
|
|
1760
1782
|
/** The SEL analysis provider for the semantic-read + open-decisions levers. Absent ⇒ offline. */
|
|
1761
1783
|
provider?: AnalysisProvider;
|
|
1784
|
+
/**
|
|
1785
|
+
* Signals that a model IS available but its levers were intentionally DEFERRED for this run
|
|
1786
|
+
* (e.g. a cheap-first report pass that holds obviously-out-of-band items before spending an LLM
|
|
1787
|
+
* call). Only affects wording: a provider-less lever then reports "not evaluated (held before
|
|
1788
|
+
* the model pass)" instead of "no provider (offline)", so a deferred lever is not mistaken for a
|
|
1789
|
+
* missing/mis-configured provider. No effect on the gate — an unread lever never dispatches.
|
|
1790
|
+
*/
|
|
1791
|
+
modelDeferred?: boolean;
|
|
1762
1792
|
/** The graph-resolution seam for the scope lever. Absent ⇒ scope degrades to unknown. */
|
|
1763
1793
|
graph?: GraphScope;
|
|
1764
1794
|
/** The precedent lever. Absent ⇒ `unknown` (cold-start), which never blocks. */
|
package/dist/index.d.ts
CHANGED
|
@@ -367,6 +367,14 @@ interface AnalysisRequest {
|
|
|
367
367
|
responseSchema: z.ZodType;
|
|
368
368
|
model?: string;
|
|
369
369
|
maxTokens?: number;
|
|
370
|
+
/**
|
|
371
|
+
* Request that the backend suppress any chain-of-thought / `<think>` reasoning for THIS call.
|
|
372
|
+
* Intended for narrow structured-extraction calls where a reasoning trace adds latency but not
|
|
373
|
+
* quality. Advisory + best-effort: a provider that can honor it (e.g. Ollama's native
|
|
374
|
+
* `think:false`) does; one that cannot ignores it and answers normally. Never changes the
|
|
375
|
+
* response contract — only whether the model reasons out loud first.
|
|
376
|
+
*/
|
|
377
|
+
disableThinking?: boolean;
|
|
370
378
|
}
|
|
371
379
|
interface AnalysisResponse<T> {
|
|
372
380
|
result: T;
|
|
@@ -437,12 +445,26 @@ interface OpenAICompatibleProviderOptions {
|
|
|
437
445
|
*/
|
|
438
446
|
declare class OpenAICompatibleAnalysisProvider implements AnalysisProvider {
|
|
439
447
|
private readonly client;
|
|
448
|
+
private readonly baseUrl;
|
|
449
|
+
private readonly timeoutMs;
|
|
440
450
|
private readonly defaultModel;
|
|
441
451
|
private readonly getModel?;
|
|
442
452
|
private readonly promptSuffix;
|
|
443
453
|
private readonly jsonMode;
|
|
444
454
|
constructor(options: OpenAICompatibleProviderOptions);
|
|
445
455
|
analyze<T>(request: AnalysisRequest): Promise<AnalysisResponse<T>>;
|
|
456
|
+
/** Build the system + user message content shared by both the OpenAI-compat and native paths. */
|
|
457
|
+
private buildMessages;
|
|
458
|
+
/** Ollama's native chat route, derived from the configured `/v1` base URL. */
|
|
459
|
+
private ollamaNativeChatUrl;
|
|
460
|
+
/**
|
|
461
|
+
* Take Ollama's NATIVE `/api/chat` with `think:false` — the only way to actually suppress a
|
|
462
|
+
* Qwen3-style reasoning trace (the OpenAI-compatible `/v1` shim ignores every thinking knob).
|
|
463
|
+
* Schema is enforced via native `format`; `num_predict` bounds output. Throws on any non-2xx,
|
|
464
|
+
* missing content, or schema-parse failure so `analyze` can fall back to the OpenAI-compatible
|
|
465
|
+
* path (e.g. when the endpoint is not actually Ollama).
|
|
466
|
+
*/
|
|
467
|
+
private analyzeViaOllamaNative;
|
|
446
468
|
}
|
|
447
469
|
|
|
448
470
|
interface ClaudeCliProviderOptions {
|
|
@@ -1759,6 +1781,14 @@ interface ProbeConfig {
|
|
|
1759
1781
|
interface ProbeDeps {
|
|
1760
1782
|
/** The SEL analysis provider for the semantic-read + open-decisions levers. Absent ⇒ offline. */
|
|
1761
1783
|
provider?: AnalysisProvider;
|
|
1784
|
+
/**
|
|
1785
|
+
* Signals that a model IS available but its levers were intentionally DEFERRED for this run
|
|
1786
|
+
* (e.g. a cheap-first report pass that holds obviously-out-of-band items before spending an LLM
|
|
1787
|
+
* call). Only affects wording: a provider-less lever then reports "not evaluated (held before
|
|
1788
|
+
* the model pass)" instead of "no provider (offline)", so a deferred lever is not mistaken for a
|
|
1789
|
+
* missing/mis-configured provider. No effect on the gate — an unread lever never dispatches.
|
|
1790
|
+
*/
|
|
1791
|
+
modelDeferred?: boolean;
|
|
1762
1792
|
/** The graph-resolution seam for the scope lever. Absent ⇒ scope degrades to unknown. */
|
|
1763
1793
|
graph?: GraphScope;
|
|
1764
1794
|
/** The precedent lever. Absent ⇒ `unknown` (cold-start), which never blocks. */
|
package/dist/index.js
CHANGED
|
@@ -449,15 +449,19 @@ var DEFAULT_MAX_TOKENS2 = 8192;
|
|
|
449
449
|
var DEFAULT_TIMEOUT_MS = 9e4;
|
|
450
450
|
var OpenAICompatibleAnalysisProvider = class {
|
|
451
451
|
client;
|
|
452
|
+
baseUrl;
|
|
453
|
+
timeoutMs;
|
|
452
454
|
defaultModel;
|
|
453
455
|
getModel;
|
|
454
456
|
promptSuffix;
|
|
455
457
|
jsonMode;
|
|
456
458
|
constructor(options) {
|
|
459
|
+
this.baseUrl = options.baseUrl;
|
|
460
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
457
461
|
this.client = new import_openai.default({
|
|
458
462
|
apiKey: options.apiKey,
|
|
459
463
|
baseURL: options.baseUrl,
|
|
460
|
-
timeout:
|
|
464
|
+
timeout: this.timeoutMs
|
|
461
465
|
});
|
|
462
466
|
this.defaultModel = options.defaultModel ?? DEFAULT_MODEL2;
|
|
463
467
|
if (options.getModel !== void 0) {
|
|
@@ -471,16 +475,19 @@ var OpenAICompatibleAnalysisProvider = class {
|
|
|
471
475
|
const model = request.model ?? (resolved != null && resolved !== "" ? resolved : this.defaultModel);
|
|
472
476
|
const maxTokens = request.maxTokens ?? DEFAULT_MAX_TOKENS2;
|
|
473
477
|
const jsonSchema = zodToJsonSchema(request.responseSchema);
|
|
474
|
-
const
|
|
475
|
-
if (request.
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
478
|
+
const { systemContent, userContent } = this.buildMessages(request, jsonSchema);
|
|
479
|
+
if (request.disableThinking === true) {
|
|
480
|
+
try {
|
|
481
|
+
return await this.analyzeViaOllamaNative(
|
|
482
|
+
request,
|
|
483
|
+
model,
|
|
484
|
+
maxTokens,
|
|
485
|
+
jsonSchema,
|
|
486
|
+
systemContent,
|
|
487
|
+
userContent
|
|
488
|
+
);
|
|
489
|
+
} catch {
|
|
490
|
+
}
|
|
484
491
|
}
|
|
485
492
|
const startMs = performance.now();
|
|
486
493
|
const responseFormat = this.jsonMode ? {
|
|
@@ -492,13 +499,8 @@ var OpenAICompatibleAnalysisProvider = class {
|
|
|
492
499
|
max_tokens: maxTokens,
|
|
493
500
|
...responseFormat && { response_format: responseFormat },
|
|
494
501
|
messages: [
|
|
495
|
-
{ role: "system", content:
|
|
496
|
-
{
|
|
497
|
-
role: "user",
|
|
498
|
-
content: this.promptSuffix ? `${request.prompt}
|
|
499
|
-
|
|
500
|
-
${this.promptSuffix}` : request.prompt
|
|
501
|
-
}
|
|
502
|
+
{ role: "system", content: systemContent },
|
|
503
|
+
{ role: "user", content: userContent }
|
|
502
504
|
]
|
|
503
505
|
});
|
|
504
506
|
const latencyMs = Math.round(performance.now() - startMs);
|
|
@@ -524,6 +526,81 @@ ${this.promptSuffix}` : request.prompt
|
|
|
524
526
|
};
|
|
525
527
|
return { result, tokenUsage, model, latencyMs };
|
|
526
528
|
}
|
|
529
|
+
/** Build the system + user message content shared by both the OpenAI-compat and native paths. */
|
|
530
|
+
buildMessages(request, jsonSchema) {
|
|
531
|
+
const systemParts = [];
|
|
532
|
+
if (request.systemPrompt) systemParts.push(request.systemPrompt);
|
|
533
|
+
if (this.jsonMode) {
|
|
534
|
+
systemParts.push(
|
|
535
|
+
"Respond ONLY with the JSON object, no other text. Be concise \u2014 use short sentences in string fields and limit arrays to the most important items."
|
|
536
|
+
);
|
|
537
|
+
} else {
|
|
538
|
+
systemParts.push(
|
|
539
|
+
"You MUST respond with valid JSON matching this schema:\n" + JSON.stringify(jsonSchema, null, 2) + "\n\nRespond ONLY with the JSON object, no other text."
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
const userContent = this.promptSuffix ? `${request.prompt}
|
|
543
|
+
|
|
544
|
+
${this.promptSuffix}` : request.prompt;
|
|
545
|
+
return { systemContent: systemParts.join("\n\n"), userContent };
|
|
546
|
+
}
|
|
547
|
+
/** Ollama's native chat route, derived from the configured `/v1` base URL. */
|
|
548
|
+
ollamaNativeChatUrl() {
|
|
549
|
+
return this.baseUrl.replace(/\/v1\/?$/, "") + "/api/chat";
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Take Ollama's NATIVE `/api/chat` with `think:false` — the only way to actually suppress a
|
|
553
|
+
* Qwen3-style reasoning trace (the OpenAI-compatible `/v1` shim ignores every thinking knob).
|
|
554
|
+
* Schema is enforced via native `format`; `num_predict` bounds output. Throws on any non-2xx,
|
|
555
|
+
* missing content, or schema-parse failure so `analyze` can fall back to the OpenAI-compatible
|
|
556
|
+
* path (e.g. when the endpoint is not actually Ollama).
|
|
557
|
+
*/
|
|
558
|
+
async analyzeViaOllamaNative(request, model, maxTokens, jsonSchema, systemContent, userContent) {
|
|
559
|
+
const startMs = performance.now();
|
|
560
|
+
const controller = new AbortController();
|
|
561
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
562
|
+
try {
|
|
563
|
+
const res = await fetch(this.ollamaNativeChatUrl(), {
|
|
564
|
+
method: "POST",
|
|
565
|
+
headers: { "Content-Type": "application/json" },
|
|
566
|
+
body: JSON.stringify({
|
|
567
|
+
model,
|
|
568
|
+
think: false,
|
|
569
|
+
stream: false,
|
|
570
|
+
format: jsonSchema,
|
|
571
|
+
options: { num_predict: maxTokens },
|
|
572
|
+
messages: [
|
|
573
|
+
{ role: "system", content: systemContent },
|
|
574
|
+
{ role: "user", content: userContent }
|
|
575
|
+
]
|
|
576
|
+
}),
|
|
577
|
+
signal: controller.signal
|
|
578
|
+
});
|
|
579
|
+
if (!res.ok) {
|
|
580
|
+
throw new Error(`Ollama native /api/chat returned ${res.status}`);
|
|
581
|
+
}
|
|
582
|
+
const data = await res.json();
|
|
583
|
+
if (data.done_reason === "length") {
|
|
584
|
+
throw new Error("Ollama native /api/chat response truncated (done_reason=length)");
|
|
585
|
+
}
|
|
586
|
+
const content = data.message?.content;
|
|
587
|
+
if (!content) {
|
|
588
|
+
throw new Error("Ollama native /api/chat response missing message content");
|
|
589
|
+
}
|
|
590
|
+
const parsed = JSON.parse(content);
|
|
591
|
+
const result = request.responseSchema.parse(parsed);
|
|
592
|
+
const inputTokens = data.prompt_eval_count ?? 0;
|
|
593
|
+
const outputTokens = data.eval_count ?? 0;
|
|
594
|
+
return {
|
|
595
|
+
result,
|
|
596
|
+
tokenUsage: { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens },
|
|
597
|
+
model,
|
|
598
|
+
latencyMs: Math.round(performance.now() - startMs)
|
|
599
|
+
};
|
|
600
|
+
} finally {
|
|
601
|
+
clearTimeout(timer);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
527
604
|
};
|
|
528
605
|
|
|
529
606
|
// src/analysis-provider/claude-cli.ts
|
|
@@ -2275,7 +2352,18 @@ async function llmTiebreak(provider, prompt, fastModel) {
|
|
|
2275
2352
|
responseSchema: TiebreakSchema,
|
|
2276
2353
|
// Only include `model` when supplied (exactOptionalPropertyTypes).
|
|
2277
2354
|
...fastModel !== void 0 ? { model: fastModel } : {},
|
|
2278
|
-
|
|
2355
|
+
// A trivial/simple/moderate/complex classification does not benefit from a reasoning
|
|
2356
|
+
// trace (verified: identical verdict with thinking on vs off) — suppress it where the
|
|
2357
|
+
// backend can (Ollama native), so a reasoning model answers in ~10 tokens instead of
|
|
2358
|
+
// ~1000. No-op on backends that cannot honor it.
|
|
2359
|
+
disableThinking: true,
|
|
2360
|
+
// Headroom for reasoning models: a thinking model (e.g. Qwen3) emits a
|
|
2361
|
+
// `<think>` trace BEFORE the JSON, so a tight cap truncates mid-reasoning →
|
|
2362
|
+
// `finish_reason: length` → empty content → the catch below silently returns
|
|
2363
|
+
// the `moderate/low` fallback, masking the real verdict. `maxTokens` is a
|
|
2364
|
+
// ceiling, not a target (a non-thinking model still stops at ~14 tokens), so
|
|
2365
|
+
// this is free on the fast path and only spends tokens when reasoning occurs.
|
|
2366
|
+
maxTokens: 4096
|
|
2279
2367
|
});
|
|
2280
2368
|
return result;
|
|
2281
2369
|
} catch {
|
|
@@ -2443,7 +2531,7 @@ async function runScopingProbe(input, deps = {}) {
|
|
|
2443
2531
|
const config = { ...DEFAULT_CONFIG, ...deps.config };
|
|
2444
2532
|
const scope = await runScopeLever(input, deps.graph);
|
|
2445
2533
|
const semanticRead = await runSemanticReadLever(input, scope.value, deps);
|
|
2446
|
-
const openDecisions = await runOpenDecisionsLever(input, deps.provider);
|
|
2534
|
+
const openDecisions = await runOpenDecisionsLever(input, deps.provider, deps.modelDeferred);
|
|
2447
2535
|
const precedent = runPrecedentLever(input, semanticRead.value, deps.precedent);
|
|
2448
2536
|
const levers = { scope, semanticRead, openDecisions, precedent };
|
|
2449
2537
|
const verdict = semanticRead.value === "unknown" ? { level: "moderate", confidence: "low", signals: {}, source: "static" } : semanticRead.value;
|
|
@@ -2507,16 +2595,29 @@ async function runSemanticReadLever(input, scope, deps) {
|
|
|
2507
2595
|
return { value: "unknown", reason: `semantic-read: classify failed \u2014 ${errMsg(err)}` };
|
|
2508
2596
|
}
|
|
2509
2597
|
}
|
|
2510
|
-
async function runOpenDecisionsLever(input, provider) {
|
|
2598
|
+
async function runOpenDecisionsLever(input, provider, modelDeferred) {
|
|
2511
2599
|
if (provider === void 0) {
|
|
2512
|
-
return {
|
|
2600
|
+
return {
|
|
2601
|
+
value: "unknown",
|
|
2602
|
+
reason: modelDeferred === true ? "open-decisions: not evaluated (item held before the model pass)" : "open-decisions: no provider (offline)"
|
|
2603
|
+
};
|
|
2513
2604
|
}
|
|
2514
2605
|
try {
|
|
2515
2606
|
const { result } = await provider.analyze({
|
|
2516
2607
|
prompt: input.taskText.prompt,
|
|
2517
2608
|
systemPrompt: OPEN_DECISIONS_SYSTEM_PROMPT,
|
|
2518
2609
|
responseSchema: OpenDecisionsSchema,
|
|
2519
|
-
|
|
2610
|
+
// Suppress the reasoning trace where the backend can (Ollama native) — enumerating the
|
|
2611
|
+
// human-judgment decisions is structured extraction, not open-ended reasoning. Verified to
|
|
2612
|
+
// surface the same decisions as with thinking on. No-op on backends that cannot honor it.
|
|
2613
|
+
disableThinking: true,
|
|
2614
|
+
// Headroom for reasoning models (Qwen3 et al.): they emit a `<think>` trace
|
|
2615
|
+
// before the JSON, and this open-ended decisions schema elicits more of it than
|
|
2616
|
+
// the tiny classify schema. A tight cap truncates mid-reasoning →
|
|
2617
|
+
// `finish_reason: length` → the catch degrades this lever to `unknown`, silently
|
|
2618
|
+
// blinding the open-decisions signal. `maxTokens` is a ceiling (a non-thinking
|
|
2619
|
+
// model still stops early), so this is free on the fast path.
|
|
2620
|
+
maxTokens: 4096
|
|
2520
2621
|
});
|
|
2521
2622
|
const decisions = result.openDecisions ?? [];
|
|
2522
2623
|
const note = decisions.length === 0 ? "open-decisions: none surfaced" : `open-decisions: ${decisions.length} requiring human judgment`;
|
package/dist/index.mjs
CHANGED
|
@@ -334,15 +334,19 @@ var DEFAULT_MAX_TOKENS2 = 8192;
|
|
|
334
334
|
var DEFAULT_TIMEOUT_MS = 9e4;
|
|
335
335
|
var OpenAICompatibleAnalysisProvider = class {
|
|
336
336
|
client;
|
|
337
|
+
baseUrl;
|
|
338
|
+
timeoutMs;
|
|
337
339
|
defaultModel;
|
|
338
340
|
getModel;
|
|
339
341
|
promptSuffix;
|
|
340
342
|
jsonMode;
|
|
341
343
|
constructor(options) {
|
|
344
|
+
this.baseUrl = options.baseUrl;
|
|
345
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
342
346
|
this.client = new OpenAI({
|
|
343
347
|
apiKey: options.apiKey,
|
|
344
348
|
baseURL: options.baseUrl,
|
|
345
|
-
timeout:
|
|
349
|
+
timeout: this.timeoutMs
|
|
346
350
|
});
|
|
347
351
|
this.defaultModel = options.defaultModel ?? DEFAULT_MODEL2;
|
|
348
352
|
if (options.getModel !== void 0) {
|
|
@@ -356,16 +360,19 @@ var OpenAICompatibleAnalysisProvider = class {
|
|
|
356
360
|
const model = request.model ?? (resolved != null && resolved !== "" ? resolved : this.defaultModel);
|
|
357
361
|
const maxTokens = request.maxTokens ?? DEFAULT_MAX_TOKENS2;
|
|
358
362
|
const jsonSchema = zodToJsonSchema(request.responseSchema);
|
|
359
|
-
const
|
|
360
|
-
if (request.
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
363
|
+
const { systemContent, userContent } = this.buildMessages(request, jsonSchema);
|
|
364
|
+
if (request.disableThinking === true) {
|
|
365
|
+
try {
|
|
366
|
+
return await this.analyzeViaOllamaNative(
|
|
367
|
+
request,
|
|
368
|
+
model,
|
|
369
|
+
maxTokens,
|
|
370
|
+
jsonSchema,
|
|
371
|
+
systemContent,
|
|
372
|
+
userContent
|
|
373
|
+
);
|
|
374
|
+
} catch {
|
|
375
|
+
}
|
|
369
376
|
}
|
|
370
377
|
const startMs = performance.now();
|
|
371
378
|
const responseFormat = this.jsonMode ? {
|
|
@@ -377,13 +384,8 @@ var OpenAICompatibleAnalysisProvider = class {
|
|
|
377
384
|
max_tokens: maxTokens,
|
|
378
385
|
...responseFormat && { response_format: responseFormat },
|
|
379
386
|
messages: [
|
|
380
|
-
{ role: "system", content:
|
|
381
|
-
{
|
|
382
|
-
role: "user",
|
|
383
|
-
content: this.promptSuffix ? `${request.prompt}
|
|
384
|
-
|
|
385
|
-
${this.promptSuffix}` : request.prompt
|
|
386
|
-
}
|
|
387
|
+
{ role: "system", content: systemContent },
|
|
388
|
+
{ role: "user", content: userContent }
|
|
387
389
|
]
|
|
388
390
|
});
|
|
389
391
|
const latencyMs = Math.round(performance.now() - startMs);
|
|
@@ -409,6 +411,81 @@ ${this.promptSuffix}` : request.prompt
|
|
|
409
411
|
};
|
|
410
412
|
return { result, tokenUsage, model, latencyMs };
|
|
411
413
|
}
|
|
414
|
+
/** Build the system + user message content shared by both the OpenAI-compat and native paths. */
|
|
415
|
+
buildMessages(request, jsonSchema) {
|
|
416
|
+
const systemParts = [];
|
|
417
|
+
if (request.systemPrompt) systemParts.push(request.systemPrompt);
|
|
418
|
+
if (this.jsonMode) {
|
|
419
|
+
systemParts.push(
|
|
420
|
+
"Respond ONLY with the JSON object, no other text. Be concise \u2014 use short sentences in string fields and limit arrays to the most important items."
|
|
421
|
+
);
|
|
422
|
+
} else {
|
|
423
|
+
systemParts.push(
|
|
424
|
+
"You MUST respond with valid JSON matching this schema:\n" + JSON.stringify(jsonSchema, null, 2) + "\n\nRespond ONLY with the JSON object, no other text."
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
const userContent = this.promptSuffix ? `${request.prompt}
|
|
428
|
+
|
|
429
|
+
${this.promptSuffix}` : request.prompt;
|
|
430
|
+
return { systemContent: systemParts.join("\n\n"), userContent };
|
|
431
|
+
}
|
|
432
|
+
/** Ollama's native chat route, derived from the configured `/v1` base URL. */
|
|
433
|
+
ollamaNativeChatUrl() {
|
|
434
|
+
return this.baseUrl.replace(/\/v1\/?$/, "") + "/api/chat";
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Take Ollama's NATIVE `/api/chat` with `think:false` — the only way to actually suppress a
|
|
438
|
+
* Qwen3-style reasoning trace (the OpenAI-compatible `/v1` shim ignores every thinking knob).
|
|
439
|
+
* Schema is enforced via native `format`; `num_predict` bounds output. Throws on any non-2xx,
|
|
440
|
+
* missing content, or schema-parse failure so `analyze` can fall back to the OpenAI-compatible
|
|
441
|
+
* path (e.g. when the endpoint is not actually Ollama).
|
|
442
|
+
*/
|
|
443
|
+
async analyzeViaOllamaNative(request, model, maxTokens, jsonSchema, systemContent, userContent) {
|
|
444
|
+
const startMs = performance.now();
|
|
445
|
+
const controller = new AbortController();
|
|
446
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
447
|
+
try {
|
|
448
|
+
const res = await fetch(this.ollamaNativeChatUrl(), {
|
|
449
|
+
method: "POST",
|
|
450
|
+
headers: { "Content-Type": "application/json" },
|
|
451
|
+
body: JSON.stringify({
|
|
452
|
+
model,
|
|
453
|
+
think: false,
|
|
454
|
+
stream: false,
|
|
455
|
+
format: jsonSchema,
|
|
456
|
+
options: { num_predict: maxTokens },
|
|
457
|
+
messages: [
|
|
458
|
+
{ role: "system", content: systemContent },
|
|
459
|
+
{ role: "user", content: userContent }
|
|
460
|
+
]
|
|
461
|
+
}),
|
|
462
|
+
signal: controller.signal
|
|
463
|
+
});
|
|
464
|
+
if (!res.ok) {
|
|
465
|
+
throw new Error(`Ollama native /api/chat returned ${res.status}`);
|
|
466
|
+
}
|
|
467
|
+
const data = await res.json();
|
|
468
|
+
if (data.done_reason === "length") {
|
|
469
|
+
throw new Error("Ollama native /api/chat response truncated (done_reason=length)");
|
|
470
|
+
}
|
|
471
|
+
const content = data.message?.content;
|
|
472
|
+
if (!content) {
|
|
473
|
+
throw new Error("Ollama native /api/chat response missing message content");
|
|
474
|
+
}
|
|
475
|
+
const parsed = JSON.parse(content);
|
|
476
|
+
const result = request.responseSchema.parse(parsed);
|
|
477
|
+
const inputTokens = data.prompt_eval_count ?? 0;
|
|
478
|
+
const outputTokens = data.eval_count ?? 0;
|
|
479
|
+
return {
|
|
480
|
+
result,
|
|
481
|
+
tokenUsage: { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens },
|
|
482
|
+
model,
|
|
483
|
+
latencyMs: Math.round(performance.now() - startMs)
|
|
484
|
+
};
|
|
485
|
+
} finally {
|
|
486
|
+
clearTimeout(timer);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
412
489
|
};
|
|
413
490
|
|
|
414
491
|
// src/analysis-provider/claude-cli.ts
|
|
@@ -2160,7 +2237,18 @@ async function llmTiebreak(provider, prompt, fastModel) {
|
|
|
2160
2237
|
responseSchema: TiebreakSchema,
|
|
2161
2238
|
// Only include `model` when supplied (exactOptionalPropertyTypes).
|
|
2162
2239
|
...fastModel !== void 0 ? { model: fastModel } : {},
|
|
2163
|
-
|
|
2240
|
+
// A trivial/simple/moderate/complex classification does not benefit from a reasoning
|
|
2241
|
+
// trace (verified: identical verdict with thinking on vs off) — suppress it where the
|
|
2242
|
+
// backend can (Ollama native), so a reasoning model answers in ~10 tokens instead of
|
|
2243
|
+
// ~1000. No-op on backends that cannot honor it.
|
|
2244
|
+
disableThinking: true,
|
|
2245
|
+
// Headroom for reasoning models: a thinking model (e.g. Qwen3) emits a
|
|
2246
|
+
// `<think>` trace BEFORE the JSON, so a tight cap truncates mid-reasoning →
|
|
2247
|
+
// `finish_reason: length` → empty content → the catch below silently returns
|
|
2248
|
+
// the `moderate/low` fallback, masking the real verdict. `maxTokens` is a
|
|
2249
|
+
// ceiling, not a target (a non-thinking model still stops at ~14 tokens), so
|
|
2250
|
+
// this is free on the fast path and only spends tokens when reasoning occurs.
|
|
2251
|
+
maxTokens: 4096
|
|
2164
2252
|
});
|
|
2165
2253
|
return result;
|
|
2166
2254
|
} catch {
|
|
@@ -2328,7 +2416,7 @@ async function runScopingProbe(input, deps = {}) {
|
|
|
2328
2416
|
const config = { ...DEFAULT_CONFIG, ...deps.config };
|
|
2329
2417
|
const scope = await runScopeLever(input, deps.graph);
|
|
2330
2418
|
const semanticRead = await runSemanticReadLever(input, scope.value, deps);
|
|
2331
|
-
const openDecisions = await runOpenDecisionsLever(input, deps.provider);
|
|
2419
|
+
const openDecisions = await runOpenDecisionsLever(input, deps.provider, deps.modelDeferred);
|
|
2332
2420
|
const precedent = runPrecedentLever(input, semanticRead.value, deps.precedent);
|
|
2333
2421
|
const levers = { scope, semanticRead, openDecisions, precedent };
|
|
2334
2422
|
const verdict = semanticRead.value === "unknown" ? { level: "moderate", confidence: "low", signals: {}, source: "static" } : semanticRead.value;
|
|
@@ -2392,16 +2480,29 @@ async function runSemanticReadLever(input, scope, deps) {
|
|
|
2392
2480
|
return { value: "unknown", reason: `semantic-read: classify failed \u2014 ${errMsg(err)}` };
|
|
2393
2481
|
}
|
|
2394
2482
|
}
|
|
2395
|
-
async function runOpenDecisionsLever(input, provider) {
|
|
2483
|
+
async function runOpenDecisionsLever(input, provider, modelDeferred) {
|
|
2396
2484
|
if (provider === void 0) {
|
|
2397
|
-
return {
|
|
2485
|
+
return {
|
|
2486
|
+
value: "unknown",
|
|
2487
|
+
reason: modelDeferred === true ? "open-decisions: not evaluated (item held before the model pass)" : "open-decisions: no provider (offline)"
|
|
2488
|
+
};
|
|
2398
2489
|
}
|
|
2399
2490
|
try {
|
|
2400
2491
|
const { result } = await provider.analyze({
|
|
2401
2492
|
prompt: input.taskText.prompt,
|
|
2402
2493
|
systemPrompt: OPEN_DECISIONS_SYSTEM_PROMPT,
|
|
2403
2494
|
responseSchema: OpenDecisionsSchema,
|
|
2404
|
-
|
|
2495
|
+
// Suppress the reasoning trace where the backend can (Ollama native) — enumerating the
|
|
2496
|
+
// human-judgment decisions is structured extraction, not open-ended reasoning. Verified to
|
|
2497
|
+
// surface the same decisions as with thinking on. No-op on backends that cannot honor it.
|
|
2498
|
+
disableThinking: true,
|
|
2499
|
+
// Headroom for reasoning models (Qwen3 et al.): they emit a `<think>` trace
|
|
2500
|
+
// before the JSON, and this open-ended decisions schema elicits more of it than
|
|
2501
|
+
// the tiny classify schema. A tight cap truncates mid-reasoning →
|
|
2502
|
+
// `finish_reason: length` → the catch degrades this lever to `unknown`, silently
|
|
2503
|
+
// blinding the open-decisions signal. `maxTokens` is a ceiling (a non-thinking
|
|
2504
|
+
// model still stops early), so this is free on the fast path.
|
|
2505
|
+
maxTokens: 4096
|
|
2405
2506
|
});
|
|
2406
2507
|
const decisions = result.openDecisions ?? [];
|
|
2407
2508
|
const note = decisions.length === 0 ? "open-decisions: none surfaced" : `open-decisions: ${decisions.length} requiring human judgment`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harness-engineering/intelligence",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Intelligence pipeline for spec enrichment, complexity modeling, and pre-execution simulation",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"@anthropic-ai/sdk": "^0.95.1",
|
|
41
41
|
"openai": "^6.0.0",
|
|
42
42
|
"zod": "^3.25.76",
|
|
43
|
-
"@harness-engineering/graph": "0.11.
|
|
44
|
-
"@harness-engineering/types": "0.
|
|
43
|
+
"@harness-engineering/graph": "0.11.9",
|
|
44
|
+
"@harness-engineering/types": "0.23.0"
|
|
45
45
|
},
|
|
46
46
|
"optionalDependencies": {
|
|
47
47
|
"canary-test-cli": "^5.4.0"
|