@relayflows/sdk 2.0.58 → 2.0.59
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/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +8 -0
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-worker-step.d.ts +2 -1
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +13 -0
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/budget-preflight.d.ts.map +1 -1
- package/dist/budget-preflight.js +6 -0
- package/dist/budget-preflight.js.map +1 -1
- package/dist/classify-contract.d.ts +19 -0
- package/dist/classify-contract.d.ts.map +1 -0
- package/dist/classify-contract.js +73 -0
- package/dist/classify-contract.js.map +1 -0
- package/dist/jev-client.d.ts +10 -0
- package/dist/jev-client.d.ts.map +1 -0
- package/dist/jev-client.js +95 -0
- package/dist/jev-client.js.map +1 -0
- package/dist/llm-worker.d.ts +1 -0
- package/dist/llm-worker.d.ts.map +1 -1
- package/dist/llm-worker.js +30 -0
- package/dist/llm-worker.js.map +1 -1
- package/dist/local-agent-environment.d.ts.map +1 -1
- package/dist/local-agent-environment.js +1 -0
- package/dist/local-agent-environment.js.map +1 -1
- package/dist/plugin-manifest.js +1 -1
- package/dist/plugin-manifest.js.map +1 -1
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +4 -1
- package/dist/preflight.js.map +1 -1
- package/package.json +2 -2
- package/src/authored-flow-executor.ts +13 -0
- package/src/authored-worker-step.ts +15 -1
- package/src/budget-preflight.ts +6 -0
- package/src/classify-contract.ts +63 -0
- package/src/jev-client.ts +78 -0
- package/src/llm-worker.ts +30 -0
- package/src/local-agent-environment.ts +1 -0
- package/src/plugin-manifest.ts +1 -1
- package/src/preflight.ts +3 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ClassifyOptions, ClassifyResult } from '@relayflows/surface';
|
|
2
|
+
import { classifyOptions, JEV_MODEL } from './classify-contract.js';
|
|
3
|
+
import type { CompletionReason } from './protocol.js';
|
|
4
|
+
|
|
5
|
+
export class JevError extends Error {
|
|
6
|
+
constructor(readonly diagnostic: string, readonly completionReason: CompletionReason = 'worker_error') {
|
|
7
|
+
super(`f.classify: ${diagnostic}`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Errors deliberately contain no provider body, request text, or credential. */
|
|
12
|
+
export async function askJev(
|
|
13
|
+
raw: ClassifyOptions, environment: NodeJS.ProcessEnv = process.env, signal?: AbortSignal,
|
|
14
|
+
): Promise<ClassifyResult> {
|
|
15
|
+
const options = classifyOptions(raw);
|
|
16
|
+
signal?.throwIfAborted();
|
|
17
|
+
try {
|
|
18
|
+
const key = environment['TYPESAFE_API_KEY'];
|
|
19
|
+
if (!key?.trim()) throw new JevError('typesafe_api_key_missing');
|
|
20
|
+
const deadline = AbortSignal.timeout(options.timeoutMs);
|
|
21
|
+
const combined = signal === undefined ? deadline : AbortSignal.any([deadline, signal]);
|
|
22
|
+
try {
|
|
23
|
+
const response = await globalThis.fetch('https://api.typesafe.ai/v1/systemone', {
|
|
24
|
+
method: 'POST', redirect: 'manual', signal: combined,
|
|
25
|
+
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
|
|
26
|
+
body: JSON.stringify({ model: JEV_MODEL, state: options.state, questions: {
|
|
27
|
+
classification: { type: 'choice', instructions: options.question, criteria: options.options },
|
|
28
|
+
} }),
|
|
29
|
+
});
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
void response.body?.cancel().catch(() => undefined);
|
|
32
|
+
throw new JevError(`jev_http_${response.status}`);
|
|
33
|
+
}
|
|
34
|
+
const body = await boundedBody(response);
|
|
35
|
+
const answer = body?.answers?.classification;
|
|
36
|
+
if (answer?.type !== 'choice' || typeof answer.choice !== 'string'
|
|
37
|
+
|| !Object.hasOwn(options.options, answer.choice) || typeof answer.confidence !== 'number'
|
|
38
|
+
|| !Number.isFinite(answer.confidence) || answer.confidence < 0 || answer.confidence > 1) {
|
|
39
|
+
throw new JevError('jev_invalid_answer', 'verification_failed');
|
|
40
|
+
}
|
|
41
|
+
if (answer.confidence < options.minConfidence) throw new JevError('jev_low_confidence', 'verification_failed');
|
|
42
|
+
return { choice: answer.choice, confidence: answer.confidence, completionReason: 'success' };
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// Losing the worker lease is never permission to complete with fallback.
|
|
45
|
+
signal?.throwIfAborted();
|
|
46
|
+
if (deadline.aborted) throw new JevError('jev_timeout', 'timeout');
|
|
47
|
+
if (error instanceof JevError) throw error;
|
|
48
|
+
throw new JevError('jev_network_error');
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (!(error instanceof JevError) || options.fallback === undefined) throw error;
|
|
52
|
+
return { choice: options.fallback, confidence: 0, completionReason: 'success', fallbackReason: error.diagnostic };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function boundedBody(response: Response): Promise<{ answers?: { classification?: { type?: unknown; choice?: unknown; confidence?: unknown } } }> {
|
|
57
|
+
const reader = response.body?.getReader();
|
|
58
|
+
if (!reader) throw new JevError('jev_invalid_answer', 'verification_failed');
|
|
59
|
+
const chunks: Uint8Array[] = [];
|
|
60
|
+
let size = 0;
|
|
61
|
+
try {
|
|
62
|
+
for (;;) {
|
|
63
|
+
const { done, value } = await reader.read();
|
|
64
|
+
if (done) break;
|
|
65
|
+
size += value.byteLength;
|
|
66
|
+
if (size > 16_384) {
|
|
67
|
+
void reader.cancel().catch(() => undefined);
|
|
68
|
+
throw new JevError('jev_response_too_large', 'verification_failed');
|
|
69
|
+
}
|
|
70
|
+
chunks.push(value);
|
|
71
|
+
}
|
|
72
|
+
} finally { reader.releaseLock(); }
|
|
73
|
+
const bytes = new Uint8Array(size);
|
|
74
|
+
let offset = 0;
|
|
75
|
+
for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
|
|
76
|
+
try { return JSON.parse(new TextDecoder().decode(bytes)); }
|
|
77
|
+
catch { throw new JevError('jev_invalid_json', 'verification_failed'); }
|
|
78
|
+
}
|
package/src/llm-worker.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { askJev, JevError } from './jev-client.js';
|
|
2
|
+
import { CLASSIFY_KIND, isClassifyStep } from './classify-contract.js';
|
|
1
3
|
import { workerSpend } from './worker-spend.js';
|
|
2
4
|
import { reportedCost } from './reported-cost.js';
|
|
3
5
|
import { DEFAULT_LOCAL_AGENT_CAPACITY } from './worker-slots.js';
|
|
@@ -53,8 +55,36 @@ export class LlmWorker extends EventEmitter {
|
|
|
53
55
|
void running.finally(() => { this.inFlight.delete(running); });
|
|
54
56
|
};
|
|
55
57
|
|
|
58
|
+
private async classify(dispatch: StepDispatchEvent, prompt: string): Promise<void> {
|
|
59
|
+
let output: unknown;
|
|
60
|
+
let diagnostic: string | undefined;
|
|
61
|
+
let reason: CompletionReason = 'success';
|
|
62
|
+
await withWorkerLease(this.client, dispatch, async signal => {
|
|
63
|
+
try {
|
|
64
|
+
const { type, ...options } = JSON.parse(prompt);
|
|
65
|
+
if (type !== CLASSIFY_KIND) throw new TypeError('invalid classification');
|
|
66
|
+
output = await askJev(options, this.environment ?? process.env, signal);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
signal.throwIfAborted();
|
|
69
|
+
diagnostic = error instanceof JevError ? error.diagnostic : 'jev_invalid_options';
|
|
70
|
+
reason = error instanceof JevError ? error.completionReason : 'verification_failed';
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
// The provider does not return usage or price; leave cost unknown.
|
|
74
|
+
await this.client.stepComplete(dispatch.run_id, dispatch.step_id, dispatch.attempt,
|
|
75
|
+
dispatch.idempotency_key, reason, {
|
|
76
|
+
output: output ?? { diagnostic },
|
|
77
|
+
usage: { tokens_in: 0, tokens_out: 0, dollars_unmetered: true },
|
|
78
|
+
...(diagnostic === undefined ? {} : { trajectory_tail: { error: `f.classify: ${diagnostic}` } }),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
56
82
|
private async execute(dispatch: StepDispatchEvent): Promise<void> {
|
|
57
83
|
const spec = dispatch.spec as ResolvedKernelLlmStep;
|
|
84
|
+
if (isClassifyStep(spec)) {
|
|
85
|
+
await this.classify(dispatch, spec.prompt);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
58
88
|
const schema = spec.verification?.json_schema;
|
|
59
89
|
const prompt = schema === undefined ? spec.prompt
|
|
60
90
|
: `${spec.prompt}\n\nReturn only a JSON value matching this JSON Schema (no Markdown fences):\n${JSON.stringify(schema)}`;
|
package/src/plugin-manifest.ts
CHANGED
|
@@ -39,7 +39,7 @@ export interface PluginManifest {
|
|
|
39
39
|
preflight: { credentials: readonly string[]; servers: readonly string[] };
|
|
40
40
|
}
|
|
41
41
|
const identifier = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
42
|
-
const reserved = new Set(['run', 'llm', 'agent', 'message', 'release', 'human', 'dispatch', 'done', 'cloud', 'memory', 'mcp', 'slack', '__proto__', 'constructor', 'prototype', 'then']);
|
|
42
|
+
const reserved = new Set(['run', 'llm', 'classify', 'agent', 'message', 'release', 'human', 'dispatch', 'done', 'cloud', 'memory', 'mcp', 'slack', '__proto__', 'constructor', 'prototype', 'then']);
|
|
43
43
|
const object = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
44
44
|
const strings = (v: unknown): v is string[] => Array.isArray(v) && v.every(s => typeof s === 'string' && s.trim().length > 0);
|
|
45
45
|
export function pluginPackageName(name: string): string {
|
package/src/preflight.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isClassifyStep } from './classify-contract.js';
|
|
1
2
|
import { brokerInstruction } from './communication/broker-spec.js';
|
|
2
3
|
import { communicationInstruction } from './communication/spec.js';
|
|
3
4
|
import { loadPlugins, type LoadedPlugin } from './plugin-loader.js';
|
|
@@ -278,6 +279,7 @@ export function resolvePreflight(flow: unknown, options: ResolutionOptions): Pre
|
|
|
278
279
|
for (const step of compiled.steps) {
|
|
279
280
|
if (step.type === 'deterministic') continue;
|
|
280
281
|
if (step.type === 'agent' && helperCall(step) !== undefined) continue;
|
|
282
|
+
if (isClassifyStep(step, compiled.cli)) continue;
|
|
281
283
|
const resolution = resolveCli(step, compiled, options.projectCli);
|
|
282
284
|
if (resolution === undefined) {
|
|
283
285
|
cliResolutionDiagnostics.push({
|
|
@@ -327,7 +329,7 @@ function preflightSync(flow: unknown, options: PreflightOptions): PreflightResul
|
|
|
327
329
|
probeNamedGate(step, options.probes, diagnostics);
|
|
328
330
|
warnOnVacuousGate(step, diagnostics);
|
|
329
331
|
warnOnUnprovableEffects(step, options.probes, diagnostics);
|
|
330
|
-
if (step.type === 'deterministic') continue;
|
|
332
|
+
if (step.type === 'deterministic' || isClassifyStep(step, compiled.cli)) continue;
|
|
331
333
|
const helper = step.type === 'agent' ? helperCall(step) : undefined;
|
|
332
334
|
if (helper !== undefined) {
|
|
333
335
|
try {
|