@relayflows/sdk 2.0.58 → 2.0.60

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 (42) 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/hosted-extension-sandbox.js +1 -1
  16. package/dist/jev-client.d.ts +10 -0
  17. package/dist/jev-client.d.ts.map +1 -0
  18. package/dist/jev-client.js +95 -0
  19. package/dist/jev-client.js.map +1 -0
  20. package/dist/llm-worker.d.ts +1 -0
  21. package/dist/llm-worker.d.ts.map +1 -1
  22. package/dist/llm-worker.js +30 -0
  23. package/dist/llm-worker.js.map +1 -1
  24. package/dist/local-agent-environment.d.ts.map +1 -1
  25. package/dist/local-agent-environment.js +1 -0
  26. package/dist/local-agent-environment.js.map +1 -1
  27. package/dist/plugin-manifest.js +1 -1
  28. package/dist/plugin-manifest.js.map +1 -1
  29. package/dist/preflight.d.ts.map +1 -1
  30. package/dist/preflight.js +4 -1
  31. package/dist/preflight.js.map +1 -1
  32. package/package.json +11 -6
  33. package/src/authored-flow-executor.ts +13 -0
  34. package/src/authored-worker-step.ts +15 -1
  35. package/src/budget-preflight.ts +6 -0
  36. package/src/classify-contract.ts +63 -0
  37. package/src/hosted-extension-sandbox.ts +1 -1
  38. package/src/jev-client.ts +78 -0
  39. package/src/llm-worker.ts +30 -0
  40. package/src/local-agent-environment.ts +1 -0
  41. package/src/plugin-manifest.ts +1 -1
  42. package/src/preflight.ts +3 -1
@@ -0,0 +1,63 @@
1
+ import type { ClassifyOptions, ClassifyResult } from '@relayflows/surface';
2
+ import { snapshotJsonValue } from './json-value.js';
3
+
4
+ export const JEV_MODEL = 'jev-latest';
5
+ export const CLASSIFY_KIND = 'typesafe.classify';
6
+ export const CLASSIFY_BUDGET_WARNING = 'Jev classification reports no token or dollar usage. Token and dollar ceilings cannot bound its spend; timeoutMs bounds its HTTP duration.';
7
+
8
+ /** Copy in fixed field order: journal reads sort keys, admission binds bytes. */
9
+ export function classifyOptions(raw: unknown): Required<Omit<ClassifyOptions, 'fallback'>> & { fallback?: string } {
10
+ // At most 39 nodes at depth 2; 560 KB covers even six-byte JSON escaping
11
+ // of every character in the largest valid question, state and 32 criteria.
12
+ const value = snapshotJsonValue(raw, 'f.classify options', {
13
+ maxDepth: 2, maxNodes: 40, maxBytes: 560_000,
14
+ }) as unknown as ClassifyOptions;
15
+ const invalid = (detail: string): never => { throw new TypeError(`f.classify ${detail}`); };
16
+ if (!value || typeof value !== 'object' || Array.isArray(value)
17
+ || Object.keys(value).some(k => !['question', 'state', 'options', 'minConfidence', 'fallback', 'timeoutMs'].includes(k))) {
18
+ invalid('accepts question, state, options, minConfidence, fallback, and timeoutMs.');
19
+ }
20
+ if (typeof value.question !== 'string' || !value.question.trim() || value.question.length > 4000) invalid('question must contain 1–4000 characters.');
21
+ if (typeof value.state !== 'string' || value.state.length > 20_000) invalid('state must be a string of at most 20,000 characters.');
22
+ if (!value.options || typeof value.options !== 'object' || Array.isArray(value.options)) invalid('options must map choices to criteria.');
23
+ const entries = Object.entries(value.options).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
24
+ if (entries.length < 2 || entries.length > 32 || entries.some(([key, criteria]) =>
25
+ !key.trim() || key.length > 100 || typeof criteria !== 'string' || !criteria.trim() || criteria.length > 2000)) {
26
+ invalid('requires 2–32 named choices (1–100 characters) with criteria (1–2000 characters).');
27
+ }
28
+ const minConfidence = value.minConfidence === undefined ? 0.6 : value.minConfidence;
29
+ if (!Number.isFinite(minConfidence) || minConfidence < 0 || minConfidence > 1) invalid('minConfidence must be between 0 and 1.');
30
+ const timeoutMs = value.timeoutMs === undefined ? 8000 : value.timeoutMs;
31
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30_000) invalid('timeoutMs must be an integer from 1 to 30000.');
32
+ if (value.fallback !== undefined && (typeof value.fallback !== 'string' || !Object.hasOwn(value.options, value.fallback))) invalid('fallback must name a declared choice.');
33
+ return {
34
+ question: value.question, state: value.state, options: Object.fromEntries(entries), minConfidence,
35
+ timeoutMs, ...(value.fallback === undefined ? {} : { fallback: value.fallback }),
36
+ };
37
+ }
38
+
39
+ export function classifyPrompt(options: unknown): string {
40
+ return JSON.stringify({ type: CLASSIFY_KIND, ...classifyOptions(options) });
41
+ }
42
+
43
+ /** Recognize the SDK's direct Jev protocol without selecting a project CLI. */
44
+ export function isClassifyStep(step: { type: string; model?: string; cli?: string; prompt?: string }, flowCli?: string): boolean {
45
+ if (step.type !== 'llm' || step.model !== JEV_MODEL || step.cli !== undefined || flowCli !== undefined || typeof step.prompt !== 'string') return false;
46
+ try { return JSON.parse(step.prompt)?.type === CLASSIFY_KIND; }
47
+ catch { return false; }
48
+ }
49
+
50
+ /** Refuse a malformed or foreign successful journal value before typed branching. */
51
+ export function readClassifyResult(value: unknown, rawOptions: unknown): ClassifyResult | undefined {
52
+ const options = classifyOptions(rawOptions);
53
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined;
54
+ const result = value as Record<string, unknown>;
55
+ const { choice, confidence, completionReason, fallbackReason } = result;
56
+ if (completionReason !== 'success' || typeof choice !== 'string' || !Object.hasOwn(options.options, choice)
57
+ || typeof confidence !== 'number' || !Number.isFinite(confidence) || confidence < 0 || confidence > 1) return undefined;
58
+ if (fallbackReason !== undefined) {
59
+ if (typeof fallbackReason !== 'string' || !/^[a-z0-9_]{1,64}$/.test(fallbackReason)
60
+ || options.fallback !== choice || confidence !== 0) return undefined;
61
+ } else if (confidence < options.minConfidence) return undefined;
62
+ return { choice, confidence, completionReason: 'success', ...(fallbackReason === undefined ? {} : { fallbackReason }) };
63
+ }
@@ -125,7 +125,7 @@ const ADDRESS_SPACE_BYTES = 16 * 1024 * 1024 * 1024;
125
125
  const DATA_BYTES = 3 * 1024 * 1024 * 1024;
126
126
  const SURFACE_RUNTIME_SHA256 = OBJECT_FREEZE({
127
127
  'flow.js': '2ba972849bf435bba36b918c348efeab71ec34d180cec7a722e535f482687a6a',
128
- 'helpers/providers.js': '4eb06d0d85ca0a3434bb2dbba7407e3d95eef2dfbedaeb0e2de0c0c0ea812457',
128
+ 'helpers/providers.js': 'cba2c6767673b47047c6cb9549c63e797b5a9b1af53e293c50ea028f64d21025',
129
129
  'provider-trigger.js': 'e2664c65397f93fb486eb6f1e756c7cec3f88b3851d79c23567cad986f80f1ff',
130
130
  'schedule.js': '8fe72f176a75ec0b5f26e12db7a597575c259a2e2cbb59690f9dc20a5e63940b',
131
131
  'triggers.js': '421609a6ea6d2ea77051dd4826a32e773b7db81dedd1dde91f4187b382cd46e3',
@@ -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 {