pi-harness-runtime 1.1.49 → 1.1.71-3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -9
- package/README.md +119 -0
- package/harness/langchain/agents.d.ts +143 -0
- package/harness/langchain/agents.test.d.ts +2 -0
- package/harness/langchain/agents.test.ts +0 -1
- package/harness/langchain/agents.ts +196 -13
- package/harness/langchain/checkpointer.d.ts +48 -0
- package/harness/langchain/checkpointer.test.d.ts +8 -0
- package/harness/langchain/cron-cli.d.ts +12 -0
- package/harness/langchain/cron-cli.ts +169 -0
- package/harness/langchain/daemon.d.ts +161 -0
- package/harness/langchain/daemon.test.d.ts +11 -0
- package/harness/langchain/daemon.test.ts +62 -5
- package/harness/langchain/daemon.ts +231 -12
- package/harness/langchain/file-tools.d.ts +97 -0
- package/harness/langchain/file-tools.ts +29 -11
- package/harness/langchain/graph.d.ts +706 -0
- package/harness/langchain/graph.ts +184 -26
- package/harness/langchain/model-router.d.ts +63 -0
- package/harness/langchain/model-router.test.d.ts +2 -0
- package/harness/langchain/model-router.test.ts +191 -0
- package/harness/langchain/model-router.ts +188 -0
- package/harness/langchain/models.d.ts +72 -0
- package/harness/langchain/models.ts +111 -49
- package/harness/langchain/ping-pong-middleware.d.ts +28 -0
- package/harness/langchain/ping-pong-middleware.test.d.ts +2 -0
- package/harness/langchain/ping-pong-middleware.test.ts +69 -0
- package/harness/langchain/ping-pong-middleware.ts +204 -0
- package/harness/langchain/run.d.ts +15 -0
- package/harness/langchain/run.ts +15 -3
- package/harness/langchain/scoreboard.test.d.ts +13 -0
- package/harness/langchain/scoreboard.test.ts +28 -5
- package/harness/langchain/status-line.d.ts +91 -0
- package/harness/langchain/status-line.test.d.ts +8 -0
- package/harness/langchain/surge.d.ts +103 -0
- package/harness/langchain/surge.test.d.ts +10 -0
- package/harness/langchain/surge.test.ts +179 -0
- package/harness/langchain/surge.ts +188 -19
- package/harness/langchain/widget.d.ts +105 -0
- package/harness/langchain/widget.test.d.ts +8 -0
- package/harness/ssh-detach-interceptor.test.ts +263 -0
- package/harness/ssh-detach-interceptor.ts +438 -0
- package/index.ts +212 -47
- package/package.json +4 -2
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/dist/src/index.d.ts +2 -0
- package/packages/intent-analyzer/dist/src/index.d.ts.map +1 -1
- package/packages/intent-analyzer/dist/src/index.js +1 -0
- package/packages/intent-analyzer/dist/src/index.js.map +1 -1
- package/packages/intent-analyzer/dist/src/ping-pong.d.ts +56 -0
- package/packages/intent-analyzer/dist/src/ping-pong.d.ts.map +1 -0
- package/packages/intent-analyzer/dist/src/ping-pong.js +286 -0
- package/packages/intent-analyzer/dist/src/ping-pong.js.map +1 -0
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/intent-analyzer/src/index.ts +13 -0
- package/packages/intent-analyzer/src/ping-pong.ts +387 -0
- package/packages/intent-analyzer/test/ping-pong.test.ts +96 -0
- package/packages/jev-judge/README.md +234 -0
- package/packages/jev-judge/dist/decision-engine.d.ts +100 -0
- package/packages/jev-judge/dist/decision-engine.d.ts.map +1 -0
- package/packages/jev-judge/dist/decision-engine.js +151 -0
- package/packages/jev-judge/dist/decision-engine.js.map +1 -0
- package/packages/jev-judge/dist/e2e-judge.d.ts +61 -0
- package/packages/jev-judge/dist/e2e-judge.d.ts.map +1 -0
- package/packages/jev-judge/dist/e2e-judge.js +268 -0
- package/packages/jev-judge/dist/e2e-judge.js.map +1 -0
- package/packages/jev-judge/dist/flaky-detector.d.ts +34 -0
- package/packages/jev-judge/dist/flaky-detector.d.ts.map +1 -0
- package/packages/jev-judge/dist/flaky-detector.js +154 -0
- package/packages/jev-judge/dist/flaky-detector.js.map +1 -0
- package/packages/jev-judge/dist/index.d.ts +77 -0
- package/packages/jev-judge/dist/index.d.ts.map +1 -0
- package/packages/jev-judge/dist/index.js +271 -0
- package/packages/jev-judge/dist/index.js.map +1 -0
- package/packages/jev-judge/dist/types.d.ts +97 -0
- package/packages/jev-judge/dist/types.d.ts.map +1 -0
- package/packages/jev-judge/dist/types.js +8 -0
- package/packages/jev-judge/dist/types.js.map +1 -0
- package/packages/jev-judge/package.json +46 -0
- package/packages/jev-judge/src/decision-engine.ts +231 -0
- package/packages/jev-judge/src/e2e-judge.ts +364 -0
- package/packages/jev-judge/src/flaky-detector.ts +190 -0
- package/packages/jev-judge/src/index.ts +345 -0
- package/packages/jev-judge/src/types.ts +131 -0
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/README.md +71 -0
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/todo-bd-sync/src/extension.ts +16 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/trajectory/dist/src/approved-patterns.d.ts +51 -0
- package/packages/trajectory/dist/src/approved-patterns.d.ts.map +1 -0
- package/packages/trajectory/dist/src/approved-patterns.js +134 -0
- package/packages/trajectory/dist/src/approved-patterns.js.map +1 -0
- package/packages/trajectory/dist/src/index.d.ts +13 -0
- package/packages/trajectory/dist/src/index.d.ts.map +1 -0
- package/packages/trajectory/dist/src/index.js +12 -0
- package/packages/trajectory/dist/src/index.js.map +1 -0
- package/packages/trajectory/dist/src/store.d.ts +69 -0
- package/packages/trajectory/dist/src/store.d.ts.map +1 -0
- package/packages/trajectory/dist/src/store.js +253 -0
- package/packages/trajectory/dist/src/store.js.map +1 -0
- package/packages/trajectory/dist/src/types.d.ts +73 -0
- package/packages/trajectory/dist/src/types.d.ts.map +1 -0
- package/packages/trajectory/dist/src/types.js +9 -0
- package/packages/trajectory/dist/src/types.js.map +1 -0
- package/packages/trajectory/dist/test/store.test.d.ts +2 -0
- package/packages/trajectory/dist/test/store.test.d.ts.map +1 -0
- package/packages/trajectory/dist/test/store.test.js +228 -0
- package/packages/trajectory/dist/test/store.test.js.map +1 -0
- package/packages/trajectory/package.json +29 -0
- package/packages/trajectory/src/approved-patterns.ts +180 -0
- package/packages/trajectory/src/index.ts +26 -0
- package/packages/trajectory/src/store.ts +285 -0
- package/packages/trajectory/src/types.ts +84 -0
- package/packages/trajectory/test/store.test.d.ts +2 -0
- package/packages/trajectory/test/store.test.ts +250 -0
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- package/packages/write-review/src/blackboard.ts +38 -5
- package/packages/write-review/src/types.ts +13 -0
- package/skills/harness-runtime/SKILL.md +67 -4
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
readdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
statSync,
|
|
6
|
+
} from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { IntentAnalyzer } from "./analyzer.js";
|
|
9
|
+
import type { Intent, IntentKind } from "./types.js";
|
|
10
|
+
|
|
11
|
+
export type PingPongDecision =
|
|
12
|
+
| "run_ping_pong"
|
|
13
|
+
| "suggest_ping_pong"
|
|
14
|
+
| "handle_inline";
|
|
15
|
+
|
|
16
|
+
export interface CodebaseComplexityInput {
|
|
17
|
+
/** Approximate total source/config/documentation files in the current repo. */
|
|
18
|
+
fileCount?: number;
|
|
19
|
+
/** Number of top-level packages/apps/workspaces. */
|
|
20
|
+
packageCount?: number;
|
|
21
|
+
/** Number of framework/runtime signals found, such as package.json, Cargo.toml, or pyproject.toml. */
|
|
22
|
+
frameworkSignalCount?: number;
|
|
23
|
+
/** Number of changed files in the working tree, if known. */
|
|
24
|
+
changedFileCount?: number;
|
|
25
|
+
/** Paths the user explicitly mentioned. */
|
|
26
|
+
mentionedPaths?: string[];
|
|
27
|
+
/** True when tests already exist and changes should usually preserve them. */
|
|
28
|
+
hasTests?: boolean;
|
|
29
|
+
/** True when release/build automation exists. */
|
|
30
|
+
hasCiOrRelease?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PingPongDecisionOptions {
|
|
34
|
+
/** Project-level complexity signals. Pass scanCodebaseComplexity(cwd) when available. */
|
|
35
|
+
codebase?: CodebaseComplexityInput;
|
|
36
|
+
/** Lower thresholds for fully autonomous sessions. */
|
|
37
|
+
autonomous?: boolean;
|
|
38
|
+
/** Allow exact user override words. Default: true. */
|
|
39
|
+
allowExplicitOverride?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface PingPongSignal {
|
|
43
|
+
id: string;
|
|
44
|
+
label: string;
|
|
45
|
+
weight: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PingPongDecisionResult {
|
|
49
|
+
decision: PingPongDecision;
|
|
50
|
+
score: number;
|
|
51
|
+
thresholds: {
|
|
52
|
+
run: number;
|
|
53
|
+
suggest: number;
|
|
54
|
+
};
|
|
55
|
+
intent: Intent;
|
|
56
|
+
signals: PingPongSignal[];
|
|
57
|
+
reasons: string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const CODE_CHANGE_INTENTS = new Set<IntentKind>([
|
|
61
|
+
"bug_fix",
|
|
62
|
+
"feature",
|
|
63
|
+
"refactor",
|
|
64
|
+
"testing",
|
|
65
|
+
"migration",
|
|
66
|
+
"security",
|
|
67
|
+
"performance",
|
|
68
|
+
"deployment",
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
const INLINE_INTENTS = new Set<IntentKind>([
|
|
72
|
+
"learning",
|
|
73
|
+
"research",
|
|
74
|
+
"code_review",
|
|
75
|
+
"documentation",
|
|
76
|
+
"general",
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
const ACTION_PATTERNS: Array<[RegExp, string, number]> = [
|
|
80
|
+
[/\b(implement|build|create|add|introduce)\b/i, "implementation request", 2],
|
|
81
|
+
[/\b(fix|debug|repair|patch|resolve)\b/i, "bug-fix request", 2],
|
|
82
|
+
[/\b(refactor|rewrite|restructure|migrate|convert|port)\b/i, "structural change request", 3],
|
|
83
|
+
[/\b(test|spec|coverage|e2e|integration)\b/i, "test-impacting request", 1],
|
|
84
|
+
[/\b(release|deploy|publish|ship)\b/i, "release-impacting request", 2],
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const COMPLEXITY_PATTERNS: Array<[RegExp, string, number]> = [
|
|
88
|
+
[/\b(system|architecture|daemon|runtime|orchestrator|scheduler|graph|loop|workflow|pipeline)\b/i, "system/architecture area", 3],
|
|
89
|
+
[/\b(concurrent|parallel|async|queue|event|bus|lease|worker|agent|multi[- ]?agent)\b/i, "coordination or concurrency", 3],
|
|
90
|
+
[/\b(cross[- ]?package|workspace|monorepo|package|plugin|extension)\b/i, "cross-package surface", 2],
|
|
91
|
+
[/\b(auth|token|secret|permission|security|quota|billing|cost)\b/i, "sensitive or quota-sensitive area", 2],
|
|
92
|
+
[/\b(database|migration|schema|cache|state|checkpoint|persistence)\b/i, "stateful change area", 2],
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const INLINE_PATTERNS: Array<[RegExp, string, number]> = [
|
|
96
|
+
[/\b(explain|what is|how does|why|analyze|investigate|understand)\b/i, "analysis-only wording", -4],
|
|
97
|
+
[/\b(no code|do not edit|don't edit|only explain|just explain|read only|read-only)\b/i, "explicit no-edit wording", -8],
|
|
98
|
+
[/\b(show|list|status|inspect|summarize)\b/i, "inspection wording", -2],
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
const EXPLICIT_RUN = /\b(ping[- ]?pong|write[- ]?review|plan\s*->\s*cod(?:e|ing)|review\s*->\s*fix|write\s+(?:down\s+)?(?:the\s+)?plan\s+to\s+wiki|ask\s+minimax\s+to\s+(?:write|code|implement)|(?:then\s+)?you\s+review)\b/i;
|
|
102
|
+
const EXPLICIT_SKIP = /\b(no ping[- ]?pong|skip ping[- ]?pong|do not run ping[- ]?pong|don't run ping[- ]?pong)\b/i;
|
|
103
|
+
|
|
104
|
+
function pushSignal(
|
|
105
|
+
signals: PingPongSignal[],
|
|
106
|
+
id: string,
|
|
107
|
+
label: string,
|
|
108
|
+
weight: number,
|
|
109
|
+
): void {
|
|
110
|
+
signals.push({ id, label, weight });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function countPathMentions(text: string): string[] {
|
|
114
|
+
const matches = text.match(
|
|
115
|
+
/(?:^|\s)((?:\.\/|\/)?(?:src|lib|packages|apps|harness|test|tests|docs|wiki|scripts|config|src-ui|src-tauri)[/\w.-]*\.[a-z0-9]+)/gi,
|
|
116
|
+
);
|
|
117
|
+
if (!matches) return [];
|
|
118
|
+
return matches.map((m) => m.trim());
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function clampScore(score: number): number {
|
|
122
|
+
return Math.max(0, Math.min(20, score));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export class PingPongDecisionEngine {
|
|
126
|
+
private readonly intentAnalyzer: IntentAnalyzer;
|
|
127
|
+
|
|
128
|
+
constructor(intentAnalyzer = new IntentAnalyzer()) {
|
|
129
|
+
this.intentAnalyzer = intentAnalyzer;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
analyze(
|
|
133
|
+
request: string,
|
|
134
|
+
options: PingPongDecisionOptions = {},
|
|
135
|
+
): PingPongDecisionResult {
|
|
136
|
+
const allowOverride = options.allowExplicitOverride ?? true;
|
|
137
|
+
const intent = this.intentAnalyzer.analyze(request);
|
|
138
|
+
const signals: PingPongSignal[] = [];
|
|
139
|
+
let score = 0;
|
|
140
|
+
|
|
141
|
+
if (allowOverride && EXPLICIT_SKIP.test(request)) {
|
|
142
|
+
pushSignal(signals, "override.skip", "explicit skip request", -20);
|
|
143
|
+
} else if (allowOverride && EXPLICIT_RUN.test(request)) {
|
|
144
|
+
pushSignal(signals, "override.run", "explicit PING-PONG request", 20);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (CODE_CHANGE_INTENTS.has(intent.kind)) {
|
|
148
|
+
const weight = intent.confidence === "high" ? 4 : 3;
|
|
149
|
+
pushSignal(signals, `intent.${intent.kind}`, `${intent.kind} intent`, weight);
|
|
150
|
+
} else if (INLINE_INTENTS.has(intent.kind)) {
|
|
151
|
+
const weight = intent.kind === "general" ? -3 : -2;
|
|
152
|
+
pushSignal(signals, `intent.${intent.kind}`, `${intent.kind} intent`, weight);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const [pattern, label, weight] of ACTION_PATTERNS) {
|
|
156
|
+
if (pattern.test(request)) pushSignal(signals, `action.${label}`, label, weight);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const [pattern, label, weight] of COMPLEXITY_PATTERNS) {
|
|
160
|
+
if (pattern.test(request)) pushSignal(signals, `complexity.${label}`, label, weight);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
for (const [pattern, label, weight] of INLINE_PATTERNS) {
|
|
164
|
+
if (pattern.test(request)) pushSignal(signals, `inline.${label}`, label, weight);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const mentionedPaths = [
|
|
168
|
+
...(options.codebase?.mentionedPaths ?? []),
|
|
169
|
+
...countPathMentions(request),
|
|
170
|
+
];
|
|
171
|
+
if (mentionedPaths.length >= 2) {
|
|
172
|
+
pushSignal(signals, "scope.paths.multi", "multiple files mentioned", 2);
|
|
173
|
+
} else if (mentionedPaths.length === 1) {
|
|
174
|
+
pushSignal(signals, "scope.paths.single", "specific file mentioned", 1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.addCodebaseSignals(signals, options.codebase);
|
|
178
|
+
|
|
179
|
+
score = clampScore(signals.reduce((sum, signal) => sum + signal.weight, 0));
|
|
180
|
+
|
|
181
|
+
const thresholds = {
|
|
182
|
+
run: options.autonomous ? 8 : 9,
|
|
183
|
+
suggest: options.autonomous ? 5 : 6,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
let decision: PingPongDecision;
|
|
187
|
+
if (allowOverride && EXPLICIT_SKIP.test(request)) {
|
|
188
|
+
decision = "handle_inline";
|
|
189
|
+
score = 0;
|
|
190
|
+
} else if (allowOverride && EXPLICIT_RUN.test(request)) {
|
|
191
|
+
decision = "run_ping_pong";
|
|
192
|
+
score = Math.max(score, thresholds.run);
|
|
193
|
+
} else if (score >= thresholds.run) {
|
|
194
|
+
decision = "run_ping_pong";
|
|
195
|
+
} else if (score >= thresholds.suggest) {
|
|
196
|
+
decision = "suggest_ping_pong";
|
|
197
|
+
} else {
|
|
198
|
+
decision = "handle_inline";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
decision,
|
|
203
|
+
score,
|
|
204
|
+
thresholds,
|
|
205
|
+
intent,
|
|
206
|
+
signals,
|
|
207
|
+
reasons: signals
|
|
208
|
+
.filter((signal) => signal.weight !== 0)
|
|
209
|
+
.sort((a, b) => Math.abs(b.weight) - Math.abs(a.weight))
|
|
210
|
+
.map((signal) => `${signal.label} (${signal.weight > 0 ? "+" : ""}${signal.weight})`),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private addCodebaseSignals(
|
|
215
|
+
signals: PingPongSignal[],
|
|
216
|
+
codebase?: CodebaseComplexityInput,
|
|
217
|
+
): void {
|
|
218
|
+
if (!codebase) return;
|
|
219
|
+
|
|
220
|
+
if ((codebase.fileCount ?? 0) >= 1000) {
|
|
221
|
+
pushSignal(signals, "repo.files.large", "large repository", 3);
|
|
222
|
+
} else if ((codebase.fileCount ?? 0) >= 250) {
|
|
223
|
+
pushSignal(signals, "repo.files.medium", "medium repository", 2);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if ((codebase.packageCount ?? 0) >= 5) {
|
|
227
|
+
pushSignal(signals, "repo.packages.large", "many packages/apps", 3);
|
|
228
|
+
} else if ((codebase.packageCount ?? 0) >= 2) {
|
|
229
|
+
pushSignal(signals, "repo.packages.multi", "multi-package project", 2);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if ((codebase.frameworkSignalCount ?? 0) >= 4) {
|
|
233
|
+
pushSignal(signals, "repo.frameworks.many", "many framework/runtime signals", 2);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if ((codebase.changedFileCount ?? 0) >= 8) {
|
|
237
|
+
pushSignal(signals, "repo.changes.many", "many current changes", 2);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (codebase.hasTests) {
|
|
241
|
+
pushSignal(signals, "repo.tests", "test suite present", 1);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (codebase.hasCiOrRelease) {
|
|
245
|
+
pushSignal(signals, "repo.release", "release/build automation present", 1);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export interface ScanCodebaseOptions {
|
|
251
|
+
maxFiles?: number;
|
|
252
|
+
maxDepth?: number;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const SKIP_DIRS = new Set([
|
|
256
|
+
".git",
|
|
257
|
+
"node_modules",
|
|
258
|
+
"dist",
|
|
259
|
+
"build",
|
|
260
|
+
"coverage",
|
|
261
|
+
".next",
|
|
262
|
+
".turbo",
|
|
263
|
+
"target",
|
|
264
|
+
]);
|
|
265
|
+
|
|
266
|
+
const FRAMEWORK_FILES = new Set([
|
|
267
|
+
"package.json",
|
|
268
|
+
"tsconfig.json",
|
|
269
|
+
"Cargo.toml",
|
|
270
|
+
"pyproject.toml",
|
|
271
|
+
"composer.json",
|
|
272
|
+
"go.mod",
|
|
273
|
+
"bun.lock",
|
|
274
|
+
"pnpm-lock.yaml",
|
|
275
|
+
"vite.config.ts",
|
|
276
|
+
"next.config.js",
|
|
277
|
+
"tauri.conf.json",
|
|
278
|
+
]);
|
|
279
|
+
|
|
280
|
+
export function scanCodebaseComplexity(
|
|
281
|
+
root: string,
|
|
282
|
+
options: ScanCodebaseOptions = {},
|
|
283
|
+
): CodebaseComplexityInput {
|
|
284
|
+
const maxFiles = options.maxFiles ?? 4000;
|
|
285
|
+
const maxDepth = options.maxDepth ?? 6;
|
|
286
|
+
let fileCount = 0;
|
|
287
|
+
let packageCount = 0;
|
|
288
|
+
let frameworkSignalCount = 0;
|
|
289
|
+
let hasTests = false;
|
|
290
|
+
let hasCiOrRelease = false;
|
|
291
|
+
|
|
292
|
+
function visit(dir: string, depth: number): void {
|
|
293
|
+
if (fileCount >= maxFiles || depth > maxDepth) return;
|
|
294
|
+
let entries: string[];
|
|
295
|
+
try {
|
|
296
|
+
entries = readdirSync(dir);
|
|
297
|
+
} catch {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
for (const entry of entries) {
|
|
302
|
+
if (fileCount >= maxFiles) return;
|
|
303
|
+
const path = join(dir, entry);
|
|
304
|
+
let stats;
|
|
305
|
+
try {
|
|
306
|
+
stats = statSync(path);
|
|
307
|
+
} catch {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (stats.isDirectory()) {
|
|
312
|
+
if (SKIP_DIRS.has(entry)) continue;
|
|
313
|
+
if ((entry === "packages" || entry === "apps") && depth <= 1) {
|
|
314
|
+
packageCount += countChildDirs(path);
|
|
315
|
+
}
|
|
316
|
+
if (entry === "test" || entry === "tests" || entry === "__tests__") {
|
|
317
|
+
hasTests = true;
|
|
318
|
+
}
|
|
319
|
+
visit(path, depth + 1);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (!stats.isFile()) continue;
|
|
324
|
+
fileCount += 1;
|
|
325
|
+
|
|
326
|
+
if (FRAMEWORK_FILES.has(entry)) frameworkSignalCount += 1;
|
|
327
|
+
if (/\.(test|spec)\.[cm]?[jt]sx?$/.test(entry)) hasTests = true;
|
|
328
|
+
if (
|
|
329
|
+
entry === "package.json" &&
|
|
330
|
+
readJsonHasPackageScripts(path, ["build", "release", "publish"])
|
|
331
|
+
) {
|
|
332
|
+
hasCiOrRelease = true;
|
|
333
|
+
}
|
|
334
|
+
if (
|
|
335
|
+
path.includes("/.github/workflows/") ||
|
|
336
|
+
entry === "release.yml" ||
|
|
337
|
+
entry === "release.yaml"
|
|
338
|
+
) {
|
|
339
|
+
hasCiOrRelease = true;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
visit(root, 0);
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
fileCount,
|
|
348
|
+
packageCount,
|
|
349
|
+
frameworkSignalCount,
|
|
350
|
+
hasTests,
|
|
351
|
+
hasCiOrRelease,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function countChildDirs(dir: string): number {
|
|
356
|
+
try {
|
|
357
|
+
return readdirSync(dir).filter((entry) => {
|
|
358
|
+
try {
|
|
359
|
+
return statSync(join(dir, entry)).isDirectory();
|
|
360
|
+
} catch {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
}).length;
|
|
364
|
+
} catch {
|
|
365
|
+
return 0;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function readJsonHasPackageScripts(path: string, scriptNames: string[]): boolean {
|
|
370
|
+
if (!existsSync(path)) return false;
|
|
371
|
+
try {
|
|
372
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as {
|
|
373
|
+
scripts?: Record<string, unknown>;
|
|
374
|
+
};
|
|
375
|
+
const scripts = parsed.scripts ?? {};
|
|
376
|
+
return scriptNames.some((name) => typeof scripts[name] === "string");
|
|
377
|
+
} catch {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function analyzePingPongDecision(
|
|
383
|
+
request: string,
|
|
384
|
+
options: PingPongDecisionOptions = {},
|
|
385
|
+
): PingPongDecisionResult {
|
|
386
|
+
return new PingPongDecisionEngine().analyze(request, options);
|
|
387
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import { equal, ok } from "node:assert";
|
|
3
|
+
import {
|
|
4
|
+
PingPongDecisionEngine,
|
|
5
|
+
analyzePingPongDecision,
|
|
6
|
+
scanCodebaseComplexity,
|
|
7
|
+
} from "../src/ping-pong.js";
|
|
8
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
|
|
12
|
+
describe("PingPongDecisionEngine", () => {
|
|
13
|
+
const engine = new PingPongDecisionEngine();
|
|
14
|
+
|
|
15
|
+
it("runs PING-PONG for explicit plan-code-review phrasing", () => {
|
|
16
|
+
const result = engine.analyze(
|
|
17
|
+
"Good plan, then write down plan to wiki/ then I will ask Minimax to write code then you review",
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
equal(result.decision, "run_ping_pong");
|
|
21
|
+
ok(result.reasons.some((reason) => reason.includes("explicit PING-PONG")));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("runs PING-PONG for complex runtime implementation work", () => {
|
|
25
|
+
const result = engine.analyze("implement daemon graph loop recovery across packages", {
|
|
26
|
+
codebase: {
|
|
27
|
+
fileCount: 1200,
|
|
28
|
+
packageCount: 12,
|
|
29
|
+
frameworkSignalCount: 8,
|
|
30
|
+
hasTests: true,
|
|
31
|
+
hasCiOrRelease: true,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
equal(result.decision, "run_ping_pong");
|
|
36
|
+
ok(result.score >= result.thresholds.run);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("keeps analysis-only questions inline", () => {
|
|
40
|
+
const result = engine.analyze("analyze why the loop is quiet, do not edit code");
|
|
41
|
+
|
|
42
|
+
equal(result.decision, "handle_inline");
|
|
43
|
+
equal(result.score, 0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("can suggest PING-PONG for borderline code work", () => {
|
|
47
|
+
const result = engine.analyze("add tests for parser", {
|
|
48
|
+
codebase: { hasTests: true },
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
equal(result.decision, "suggest_ping_pong");
|
|
52
|
+
ok(result.score >= result.thresholds.suggest);
|
|
53
|
+
ok(result.score < result.thresholds.run);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("allows explicit skip to override code-change wording", () => {
|
|
57
|
+
const result = engine.analyze("fix the daemon bug but skip ping-pong for now");
|
|
58
|
+
|
|
59
|
+
equal(result.decision, "handle_inline");
|
|
60
|
+
equal(result.score, 0);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("scanCodebaseComplexity", () => {
|
|
65
|
+
it("extracts package, test, and release signals", () => {
|
|
66
|
+
const root = mkdtempSync(join(tmpdir(), "ping-pong-scan-"));
|
|
67
|
+
try {
|
|
68
|
+
mkdirSync(join(root, "packages", "alpha"), { recursive: true });
|
|
69
|
+
mkdirSync(join(root, "packages", "beta"), { recursive: true });
|
|
70
|
+
mkdirSync(join(root, "test"), { recursive: true });
|
|
71
|
+
writeFileSync(
|
|
72
|
+
join(root, "package.json"),
|
|
73
|
+
JSON.stringify({ scripts: { build: "tsc", release: "standard-version" } }),
|
|
74
|
+
);
|
|
75
|
+
writeFileSync(join(root, "tsconfig.json"), "{}", "utf8");
|
|
76
|
+
writeFileSync(join(root, "test", "demo.test.ts"), "", "utf8");
|
|
77
|
+
|
|
78
|
+
const result = scanCodebaseComplexity(root);
|
|
79
|
+
|
|
80
|
+
ok(result.fileCount >= 3);
|
|
81
|
+
equal(result.packageCount, 2);
|
|
82
|
+
ok(result.frameworkSignalCount >= 2);
|
|
83
|
+
equal(result.hasTests, true);
|
|
84
|
+
equal(result.hasCiOrRelease, true);
|
|
85
|
+
} finally {
|
|
86
|
+
rmSync(root, { recursive: true, force: true });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("analyzePingPongDecision", () => {
|
|
92
|
+
it("uses the default engine", () => {
|
|
93
|
+
const result = analyzePingPongDecision("ping-pong this implementation");
|
|
94
|
+
equal(result.decision, "run_ping_pong");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Jev Judge
|
|
2
|
+
|
|
3
|
+
Structured decision model integration for pi-harness-runtime. Uses [TypeSafe Jev](https://typesafe.ai) via OpenRouter for fast, calibrated, hallucination-free decisions.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Jev is a "System One" model - a new class of AI optimized for **structured decisions** rather than text generation:
|
|
8
|
+
|
|
9
|
+
| Aspect | Traditional LLM | Jev (System One) |
|
|
10
|
+
|--------|-----------------|------------------|
|
|
11
|
+
| Output | Text (can hallucinate) | Type-safe values (0% hallucination) |
|
|
12
|
+
| Speed | ~1-2s per call | **~250ms per call** |
|
|
13
|
+
| Cost | Higher | ~5x cheaper |
|
|
14
|
+
| Use case | Chat, reasoning | Classification, routing, decisions |
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **JevJudge** - Core API for structured decisions
|
|
19
|
+
- **E2EJudge** - E2E testing decisions (flaky detection, build blocking, retry)
|
|
20
|
+
- **FlakyDetector** - Specialized flaky test detection
|
|
21
|
+
- **DecisionEngine** - Threshold-based routing and actions
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Via monorepo (already included)
|
|
27
|
+
bun install
|
|
28
|
+
|
|
29
|
+
# Or install separately
|
|
30
|
+
npm install @pi-harness/jev-judge
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Configuration
|
|
34
|
+
|
|
35
|
+
Requires OpenRouter API key with TypeSafe/Jev access:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
export OPENROUTER_API_KEY="sk-or-..."
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or set in code:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
const judge = new JevJudge({
|
|
45
|
+
apiKey: process.env.OPENROUTER_API_KEY!,
|
|
46
|
+
baseUrl: "https://openrouter.ai/api/v1", // default
|
|
47
|
+
model: "typesafe/jev-1.13", // default
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
### Basic Decisions
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { JevJudge } from "@pi-harness/jev-judge";
|
|
57
|
+
|
|
58
|
+
const judge = new JevJudge({ apiKey: process.env.OPENROUTER_API_KEY! });
|
|
59
|
+
|
|
60
|
+
// Ask multiple questions at once
|
|
61
|
+
const result = await judge.evaluate(
|
|
62
|
+
"The deploy failed with exit code 1. Error: ECONNREFUSED",
|
|
63
|
+
{
|
|
64
|
+
isUrgent: {
|
|
65
|
+
type: "noul",
|
|
66
|
+
instructions: "Does this require immediate attention?"
|
|
67
|
+
},
|
|
68
|
+
severity: {
|
|
69
|
+
type: "choice",
|
|
70
|
+
instructions: "What is the severity level?",
|
|
71
|
+
options: ["critical", "high", "medium", "low"]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
console.log(result.decisions.isUrgent.probability); // 0.92
|
|
77
|
+
console.log(result.decisions.severity.response.choice); // "critical"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Task Tracker Routing (Todo vs BD)
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { Router } from "@pi-harness/jev-judge/decision-engine";
|
|
84
|
+
|
|
85
|
+
const result = await Router.routeTaskTracker(
|
|
86
|
+
"Fix the login bug on the checkout page",
|
|
87
|
+
judge
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (result.tracker === "bd") {
|
|
91
|
+
// Create GitHub issue
|
|
92
|
+
} else {
|
|
93
|
+
// Use local todo
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### E2E Test Flaky Detection
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { E2EJudge } from "@pi-harness/jev-judge/e2e-judge";
|
|
101
|
+
|
|
102
|
+
const e2eJudge = new E2EJudge({ apiKey: process.env.OPENROUTER_API_KEY! });
|
|
103
|
+
|
|
104
|
+
// Analyze a test failure
|
|
105
|
+
const flakyAnalysis = await e2eJudge.analyzeFlakiness({
|
|
106
|
+
name: "checkout-flow.spec.ts",
|
|
107
|
+
status: "failed",
|
|
108
|
+
error: "Timeout: Element #checkout-button not visible after 5000ms",
|
|
109
|
+
retries: 1
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (flakyAnalysis.isFlaky) {
|
|
113
|
+
console.log(`Flaky detected: ${flakyAnalysis.pattern}`);
|
|
114
|
+
// Auto-skip or retry
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Build Decision
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// Should this failure block the build?
|
|
122
|
+
const blockDecision = await e2eJudge.shouldBlockBuild(testResult);
|
|
123
|
+
|
|
124
|
+
if (blockDecision.shouldBlock) {
|
|
125
|
+
console.log(`Blocking build: ${blockDecision.reason}`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Threshold-Based Actions
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { DecisionEngine } from "@pi-harness/jev-judge/decision-engine";
|
|
134
|
+
|
|
135
|
+
const engine = new DecisionEngine(
|
|
136
|
+
{ apiKey: process.env.OPENROUTER_API_KEY! },
|
|
137
|
+
{ approval: 0.9, review: 0.6, block: 0.3 }
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const decision = engine.decide(0.85);
|
|
141
|
+
|
|
142
|
+
switch (decision.action) {
|
|
143
|
+
case "proceed":
|
|
144
|
+
// Auto-approve
|
|
145
|
+
break;
|
|
146
|
+
case "review":
|
|
147
|
+
// Send to human
|
|
148
|
+
break;
|
|
149
|
+
case "block":
|
|
150
|
+
// Auto-reject
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## API Reference
|
|
156
|
+
|
|
157
|
+
### JevJudge
|
|
158
|
+
|
|
159
|
+
Core class for making structured decisions.
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
const judge = new JevJudge(config: JevJudgeConfig);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### Methods
|
|
166
|
+
|
|
167
|
+
- `evaluate(state, questions)` - Evaluate state against questions
|
|
168
|
+
- `noul(state, question)` - Quick yes/no evaluation
|
|
169
|
+
- `choice(state, question, options)` - Quick choice evaluation
|
|
170
|
+
- `thresholdDecision(probability, threshold)` - Threshold-based decision
|
|
171
|
+
- `batchEvaluate(states, questions)` - Batch evaluation
|
|
172
|
+
|
|
173
|
+
### E2EJudge
|
|
174
|
+
|
|
175
|
+
Specialized judge for E2E testing.
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
const e2eJudge = new E2EJudge(config, options);
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
#### Methods
|
|
182
|
+
|
|
183
|
+
- `analyzeFlakiness(testResult)` - Detect flaky tests
|
|
184
|
+
- `shouldBlockBuild(testResult)` - Build blocking decision
|
|
185
|
+
- `shouldRetry(testResult)` - Retry recommendation
|
|
186
|
+
- `classifyError(errorMessage)` - Error classification
|
|
187
|
+
- `batchAnalyze(testResults)` - Batch test analysis
|
|
188
|
+
|
|
189
|
+
### FlakyDetector
|
|
190
|
+
|
|
191
|
+
Specialized for flaky test detection with pattern matching.
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
const detector = new FlakyDetector(config, threshold);
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### Methods
|
|
198
|
+
|
|
199
|
+
- `detectPattern(errorMessage)` - Fast pattern-based detection
|
|
200
|
+
- `detect(testResult)` - AI-powered detection
|
|
201
|
+
- `batchDetect(testResults)` - Batch detection
|
|
202
|
+
|
|
203
|
+
### DecisionEngine
|
|
204
|
+
|
|
205
|
+
Threshold-based routing and actions.
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
const engine = new DecisionEngine(config, thresholds);
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
#### Methods
|
|
212
|
+
|
|
213
|
+
- `decide(probability)` - Decide action from probability
|
|
214
|
+
- `decideWithThresholds(probabilities)` - Multi-question decisions
|
|
215
|
+
- `route(decision, handlers)` - Route to handlers
|
|
216
|
+
- `batchDecide(decisions)` - Batch decisions
|
|
217
|
+
- `weightedVote(votes)` - Weighted voting
|
|
218
|
+
|
|
219
|
+
## Pricing
|
|
220
|
+
|
|
221
|
+
Jev via OpenRouter:
|
|
222
|
+
- Input: $0.042 / 1M tokens
|
|
223
|
+
- Output: Free
|
|
224
|
+
- Latency: ~250ms P50
|
|
225
|
+
|
|
226
|
+
See [OpenRouter pricing](https://openrouter.ai/typesafe/jev-1.13) for details.
|
|
227
|
+
|
|
228
|
+
## Examples
|
|
229
|
+
|
|
230
|
+
See `tests/` directory for example usage.
|
|
231
|
+
|
|
232
|
+
## License
|
|
233
|
+
|
|
234
|
+
MIT
|