@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.
Files changed (40) hide show
  1. package/dist/authored-flow-executor.d.ts.map +1 -1
  2. package/dist/authored-flow-executor.js +8 -0
  3. package/dist/authored-flow-executor.js.map +1 -1
  4. package/dist/authored-worker-step.d.ts +2 -1
  5. package/dist/authored-worker-step.d.ts.map +1 -1
  6. package/dist/authored-worker-step.js +13 -0
  7. package/dist/authored-worker-step.js.map +1 -1
  8. package/dist/budget-preflight.d.ts.map +1 -1
  9. package/dist/budget-preflight.js +6 -0
  10. package/dist/budget-preflight.js.map +1 -1
  11. package/dist/classify-contract.d.ts +19 -0
  12. package/dist/classify-contract.d.ts.map +1 -0
  13. package/dist/classify-contract.js +73 -0
  14. package/dist/classify-contract.js.map +1 -0
  15. package/dist/jev-client.d.ts +10 -0
  16. package/dist/jev-client.d.ts.map +1 -0
  17. package/dist/jev-client.js +95 -0
  18. package/dist/jev-client.js.map +1 -0
  19. package/dist/llm-worker.d.ts +1 -0
  20. package/dist/llm-worker.d.ts.map +1 -1
  21. package/dist/llm-worker.js +30 -0
  22. package/dist/llm-worker.js.map +1 -1
  23. package/dist/local-agent-environment.d.ts.map +1 -1
  24. package/dist/local-agent-environment.js +1 -0
  25. package/dist/local-agent-environment.js.map +1 -1
  26. package/dist/plugin-manifest.js +1 -1
  27. package/dist/plugin-manifest.js.map +1 -1
  28. package/dist/preflight.d.ts.map +1 -1
  29. package/dist/preflight.js +4 -1
  30. package/dist/preflight.js.map +1 -1
  31. package/package.json +2 -2
  32. package/src/authored-flow-executor.ts +13 -0
  33. package/src/authored-worker-step.ts +15 -1
  34. package/src/budget-preflight.ts +6 -0
  35. package/src/classify-contract.ts +63 -0
  36. package/src/jev-client.ts +78 -0
  37. package/src/llm-worker.ts +30 -0
  38. package/src/local-agent-environment.ts +1 -0
  39. package/src/plugin-manifest.ts +1 -1
  40. 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)}`;
@@ -13,6 +13,7 @@ const AGENT_CREDENTIAL_NAMES = new Set([
13
13
  'ANTHROPIC_BASE_URL',
14
14
  'OPENAI_API_KEY',
15
15
  'OPENAI_BASE_URL',
16
+ 'TYPESAFE_API_KEY',
16
17
  ]);
17
18
 
18
19
  export function localAgentEnvironment(base: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv | undefined {
@@ -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 {