@devosurf/tesser-sdk 0.1.0-alpha.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.
@@ -0,0 +1,57 @@
1
+ // Retry policy resolution — one implementation so the in-process TestEngine and the
2
+ // durable server engine schedule identical retries (ADR-0008 fidelity).
3
+
4
+ import type { RetryPolicy } from "../automation.js";
5
+ import { parseDuration } from "./duration.js";
6
+
7
+ export interface ResolvedRetryPolicy {
8
+ maxAttempts: number;
9
+ type: "exponential" | "fixed";
10
+ baseMs: number;
11
+ maxMs: number;
12
+ jitter: boolean;
13
+ }
14
+
15
+ export const DEFAULT_STEP_RETRY: ResolvedRetryPolicy = {
16
+ maxAttempts: 3,
17
+ type: "exponential",
18
+ baseMs: 1000,
19
+ maxMs: 3_600_000,
20
+ jitter: true,
21
+ };
22
+
23
+ export function resolveRetryPolicy(
24
+ policy: RetryPolicy | undefined,
25
+ fallback: ResolvedRetryPolicy = DEFAULT_STEP_RETRY,
26
+ ): ResolvedRetryPolicy {
27
+ if (!policy) return fallback;
28
+ const backoff = policy.backoff;
29
+ if (backoff === undefined) return { ...fallback, maxAttempts: policy.maxAttempts };
30
+ if (typeof backoff === "string") {
31
+ return { ...fallback, maxAttempts: policy.maxAttempts, type: backoff };
32
+ }
33
+ return {
34
+ maxAttempts: policy.maxAttempts,
35
+ type: backoff.type,
36
+ baseMs: backoff.base !== undefined ? parseDuration(backoff.base, "retry.backoff.base") : fallback.baseMs,
37
+ maxMs: backoff.max !== undefined ? parseDuration(backoff.max, "retry.backoff.max") : fallback.maxMs,
38
+ jitter: backoff.jitter ?? fallback.jitter,
39
+ };
40
+ }
41
+
42
+ /** Delay before the NEXT attempt, or null when attempts are exhausted.
43
+ * `attempt` is the 1-based attempt that just failed. */
44
+ export function nextRetryDelayMs(
45
+ policy: ResolvedRetryPolicy,
46
+ attempt: number,
47
+ retryAfterMs?: number | undefined,
48
+ random: () => number = Math.random,
49
+ ): number | null {
50
+ if (attempt >= policy.maxAttempts) return null;
51
+ let delay =
52
+ policy.type === "fixed" ? policy.baseMs : policy.baseMs * Math.pow(2, attempt - 1);
53
+ delay = Math.min(delay, policy.maxMs);
54
+ if (policy.jitter) delay = delay * (0.5 + random() * 0.5);
55
+ if (retryAfterMs !== undefined) delay = Math.max(delay, retryAfterMs);
56
+ return Math.round(delay);
57
+ }
@@ -0,0 +1,92 @@
1
+ // Standard Schema v1 (https://standardschema.dev) — the spec interface, declared by us so
2
+ // the SDK stays zero-dependency and no third-party name appears in our surface (ADR-0004).
3
+ // Zod / Valibot / ArkType all implement `~standard`.
4
+
5
+ import { TerminalError } from "../errors.js";
6
+
7
+ export interface StandardSchemaV1<Input = unknown, Output = Input> {
8
+ readonly "~standard": StandardSchemaV1.Props<Input, Output>;
9
+ }
10
+
11
+ export declare namespace StandardSchemaV1 {
12
+ interface Props<Input = unknown, Output = Input> {
13
+ readonly version: 1;
14
+ readonly vendor: string;
15
+ readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
16
+ readonly types?: Types<Input, Output> | undefined;
17
+ }
18
+ type Result<Output> = SuccessResult<Output> | FailureResult;
19
+ interface SuccessResult<Output> {
20
+ readonly value: Output;
21
+ readonly issues?: undefined;
22
+ }
23
+ interface FailureResult {
24
+ readonly issues: ReadonlyArray<Issue>;
25
+ }
26
+ interface Issue {
27
+ readonly message: string;
28
+ readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
29
+ }
30
+ interface PathSegment {
31
+ readonly key: PropertyKey;
32
+ }
33
+ interface Types<Input = unknown, Output = Input> {
34
+ readonly input: Input;
35
+ readonly output: Output;
36
+ }
37
+ }
38
+
39
+ export interface ValidationIssue {
40
+ message: string;
41
+ path: string;
42
+ }
43
+
44
+ /** Validation failures are terminal — bad input never retries (ADR-0002 semantics). */
45
+ export class SchemaValidationError extends TerminalError {
46
+ constructor(
47
+ readonly what: string,
48
+ readonly issues: ValidationIssue[],
49
+ ) {
50
+ super(
51
+ `${what} failed validation: ` +
52
+ issues.map((i) => (i.path === "$" ? i.message : `${i.path}: ${i.message}`)).join("; "),
53
+ );
54
+ this.name = "SchemaValidationError";
55
+ }
56
+ }
57
+
58
+ function pathToString(path: StandardSchemaV1.Issue["path"]): string {
59
+ if (!path || path.length === 0) return "$";
60
+ return (
61
+ "$." +
62
+ path
63
+ .map((seg) => String(typeof seg === "object" && seg !== null && "key" in seg ? seg.key : seg))
64
+ .join(".")
65
+ );
66
+ }
67
+
68
+ export function isSchema(value: unknown): value is StandardSchemaV1 {
69
+ return (
70
+ typeof value === "object" &&
71
+ value !== null &&
72
+ "~standard" in value &&
73
+ typeof (value as Record<string, unknown>)["~standard"] === "object"
74
+ );
75
+ }
76
+
77
+ /** Validate `value` against a Standard Schema; throws SchemaValidationError on failure. */
78
+ export async function validateSchema<T>(
79
+ schema: StandardSchemaV1<unknown, T>,
80
+ value: unknown,
81
+ what: string,
82
+ ): Promise<T> {
83
+ let result = schema["~standard"].validate(value);
84
+ if (result instanceof Promise) result = await result;
85
+ if (result.issues) {
86
+ throw new SchemaValidationError(
87
+ what,
88
+ result.issues.map((i) => ({ message: i.message, path: pathToString(i.path) })),
89
+ );
90
+ }
91
+ return result.value;
92
+ }
@@ -0,0 +1,49 @@
1
+ // Declared-scheme webhook signature verification (ADR-0013): crypto an agent can get
2
+ // wrong, implemented once. Used by the server ingress and by the testing harness.
3
+
4
+ import { createHmac, timingSafeEqual } from "node:crypto";
5
+ import type { InboundWebhookEvent, VerifyScheme } from "../connector/index.js";
6
+
7
+ const SLACK_TOLERANCE_MS = 5 * 60 * 1000;
8
+
9
+ function safeEqual(a: string, b: string): boolean {
10
+ const ab = Buffer.from(a);
11
+ const bb = Buffer.from(b);
12
+ if (ab.length !== bb.length) return false;
13
+ return timingSafeEqual(ab, bb);
14
+ }
15
+
16
+ function header(req: InboundWebhookEvent, name: string): string | undefined {
17
+ return req.headers[name.toLowerCase()];
18
+ }
19
+
20
+ export async function verifyInboundEvent(
21
+ scheme: VerifyScheme,
22
+ req: InboundWebhookEvent,
23
+ secret: string,
24
+ nowMs = Date.now(),
25
+ ): Promise<boolean> {
26
+ switch (scheme.kind) {
27
+ case "none":
28
+ return true;
29
+ case "hmacSha256": {
30
+ const provided = header(req, scheme.header);
31
+ if (!provided) return false;
32
+ const digest = createHmac("sha256", secret).update(req.rawBody).digest(scheme.encoding);
33
+ const expected = (scheme.prefix ?? "") + digest;
34
+ return safeEqual(provided, expected);
35
+ }
36
+ case "slackSigning": {
37
+ const ts = header(req, "x-slack-request-timestamp");
38
+ const provided = header(req, "x-slack-signature");
39
+ if (!ts || !provided) return false;
40
+ const age = Math.abs(nowMs - Number(ts) * 1000);
41
+ if (!Number.isFinite(age) || age > SLACK_TOLERANCE_MS) return false;
42
+ const base = `v0:${ts}:${Buffer.from(req.rawBody).toString("utf8")}`;
43
+ const expected = "v0=" + createHmac("sha256", secret).update(base).digest("hex");
44
+ return safeEqual(provided, expected);
45
+ }
46
+ case "custom":
47
+ return scheme.verify(req, secret);
48
+ }
49
+ }
@@ -0,0 +1,181 @@
1
+ import type { Schema, Serializable } from "./automation.js";
2
+
3
+ export interface ModelSettingsV1 {
4
+ maxOutputTokens?: number;
5
+ temperature?: number;
6
+ topP?: number;
7
+ reasoning?: { effort?: "low" | "medium" | "high" };
8
+ }
9
+
10
+ export interface ModelDef {
11
+ readonly __model: true;
12
+ readonly connection: string;
13
+ readonly alias: string;
14
+ readonly settings?: ModelSettingsV1;
15
+ }
16
+
17
+ export function model(def: {
18
+ connection: string;
19
+ alias: string;
20
+ settings?: ModelSettingsV1;
21
+ }): ModelDef {
22
+ if (!def || typeof def.connection !== "string" || def.connection.length === 0) {
23
+ throw new TypeError("model: connection must name a declared connection");
24
+ }
25
+ if (typeof def.alias !== "string" || def.alias.length === 0) {
26
+ throw new TypeError("model: alias must be a non-empty string");
27
+ }
28
+ validateSettings(def.settings, `model(${def.alias})`);
29
+ return Object.freeze({
30
+ __model: true as const,
31
+ connection: def.connection,
32
+ alias: def.alias,
33
+ ...(def.settings !== undefined ? { settings: Object.freeze({ ...def.settings }) } : {}),
34
+ });
35
+ }
36
+
37
+ export interface OperatorDef<TInput = unknown, TOutput = unknown> {
38
+ readonly __operator: true;
39
+ readonly input: Schema<TInput>;
40
+ readonly output: Schema<TOutput>;
41
+ readonly model: string;
42
+ readonly instructions: string;
43
+ readonly tools: readonly string[];
44
+ readonly maxTurns: number;
45
+ }
46
+
47
+ export type ModelMap = Record<string, ModelDef>;
48
+ export type OperatorMap = Record<string, OperatorDef<any, any>>;
49
+ export type OperatorInput<O> = O extends OperatorDef<infer I, any> ? I : never;
50
+ export type OperatorOutput<O> = O extends OperatorDef<any, infer Out> ? Out : never;
51
+
52
+ export type Operators<O extends OperatorMap = OperatorMap> = {
53
+ readonly [K in keyof O]: (input: OperatorInput<O[K]>) => Promise<OperatorOutput<O[K]>>;
54
+ };
55
+
56
+ export interface ModelBudget {
57
+ tokens: number;
58
+ outputTokens: number;
59
+ }
60
+
61
+ export interface AutomationBudget {
62
+ models?: ModelBudget;
63
+ }
64
+
65
+ const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
66
+ const TOOL_PATH = /^[A-Za-z_$][A-Za-z0-9_$]*(\.[A-Za-z_$][A-Za-z0-9_$]*)+$/;
67
+
68
+ export function defineOperator<I, O>(def: {
69
+ input: Schema<I>;
70
+ output: Schema<O>;
71
+ model: string;
72
+ instructions: string;
73
+ tools?: readonly string[];
74
+ maxTurns?: number;
75
+ }): OperatorDef<I, O> {
76
+ if (!def?.input) throw new TypeError("defineOperator: input schema is required");
77
+ if (!def.output) throw new TypeError("defineOperator: output schema is required");
78
+ if (typeof def.model !== "string" || !IDENT.test(def.model)) {
79
+ throw new TypeError("defineOperator: model must name a model key, e.g. \"smart\"");
80
+ }
81
+ if (typeof def.instructions !== "string" || def.instructions.trim().length === 0) {
82
+ throw new TypeError("defineOperator: instructions must be a non-empty string");
83
+ }
84
+ const tools = [...(def.tools ?? [])];
85
+ for (const t of tools) {
86
+ if (typeof t !== "string" || !TOOL_PATH.test(t)) {
87
+ throw new TypeError(`defineOperator: tool ${JSON.stringify(t)} must be a dotted Action path like "gmail.messages.get"`);
88
+ }
89
+ }
90
+ const maxTurns = def.maxTurns ?? 4;
91
+ if (!Number.isInteger(maxTurns) || maxTurns < 1 || maxTurns > 50) {
92
+ throw new TypeError("defineOperator: maxTurns must be an integer between 1 and 50");
93
+ }
94
+ return Object.freeze({
95
+ __operator: true as const,
96
+ input: def.input,
97
+ output: def.output,
98
+ model: def.model,
99
+ instructions: def.instructions,
100
+ tools: Object.freeze(tools),
101
+ maxTurns,
102
+ });
103
+ }
104
+
105
+ function validateSettings(settings: ModelSettingsV1 | undefined, what: string): void {
106
+ if (settings === undefined) return;
107
+ if (
108
+ settings.maxOutputTokens !== undefined &&
109
+ (!Number.isInteger(settings.maxOutputTokens) || settings.maxOutputTokens < 1)
110
+ ) {
111
+ throw new TypeError(`${what}: settings.maxOutputTokens must be a positive integer`);
112
+ }
113
+ if (
114
+ settings.temperature !== undefined &&
115
+ (typeof settings.temperature !== "number" || settings.temperature < 0 || settings.temperature > 2)
116
+ ) {
117
+ throw new TypeError(`${what}: settings.temperature must be between 0 and 2`);
118
+ }
119
+ if (settings.topP !== undefined && (typeof settings.topP !== "number" || settings.topP < 0 || settings.topP > 1)) {
120
+ throw new TypeError(`${what}: settings.topP must be between 0 and 1`);
121
+ }
122
+ const effort = settings.reasoning?.effort;
123
+ if (effort !== undefined && effort !== "low" && effort !== "medium" && effort !== "high") {
124
+ throw new TypeError(`${what}: settings.reasoning.effort must be low, medium, or high`);
125
+ }
126
+ }
127
+
128
+ export function isModelDef(value: unknown): value is ModelDef {
129
+ return typeof value === "object" && value !== null && (value as ModelDef).__model === true;
130
+ }
131
+
132
+ export function isOperatorDef(value: unknown): value is OperatorDef<any, any> {
133
+ return typeof value === "object" && value !== null && (value as OperatorDef).__operator === true;
134
+ }
135
+
136
+ export interface ModelUsage {
137
+ inputTokens: number;
138
+ outputTokens: number;
139
+ reasoningTokens?: number;
140
+ }
141
+
142
+ export interface ModelToolDescriptor {
143
+ name: string;
144
+ path: string;
145
+ description?: string;
146
+ inputSchema?: Serializable;
147
+ }
148
+
149
+ export interface ModelMessage {
150
+ role: "user" | "assistant" | "tool";
151
+ content: string;
152
+ toolCallId?: string;
153
+ }
154
+
155
+ export interface ModelToolCall {
156
+ id: string;
157
+ name: string;
158
+ input: Serializable;
159
+ }
160
+
161
+ export interface NormalizedModelRequest {
162
+ operatorKey: string;
163
+ modelKey: string;
164
+ alias: string;
165
+ instructions: string;
166
+ input: Serializable;
167
+ messages: ModelMessage[];
168
+ tools: ModelToolDescriptor[];
169
+ settings?: ModelSettingsV1;
170
+ outputJsonSchema?: Serializable;
171
+ }
172
+
173
+ export interface NormalizedModelResponse {
174
+ content?: string;
175
+ output?: Serializable;
176
+ toolCalls?: ModelToolCall[];
177
+ usage: ModelUsage;
178
+ provider?: string;
179
+ model?: string;
180
+ raw?: Serializable;
181
+ }
@@ -0,0 +1,79 @@
1
+ // Triggers: prebuilt, typed constructors (ADR-0001). Exactly one per automation. The
2
+ // constructor carries plain-data config so the build extracts it without running anything.
3
+
4
+ import type { EventDefinition } from "./events.js";
5
+ import type { StandardSchemaV1 } from "./internal/standard-schema.js";
6
+
7
+ export interface Trigger<TInput> {
8
+ readonly kind: "webhook" | "schedule" | "event" | "connector";
9
+ /** Phantom for inference; never set. */
10
+ readonly _input?: TInput;
11
+ }
12
+
13
+ export interface WebhookTrigger<T> extends Trigger<T> {
14
+ readonly kind: "webhook";
15
+ readonly input?: StandardSchemaV1<unknown, T>;
16
+ readonly respond: "async" | "sync";
17
+ }
18
+
19
+ export interface ScheduleTrigger extends Trigger<void> {
20
+ readonly kind: "schedule";
21
+ readonly cron: string;
22
+ readonly tz?: string;
23
+ }
24
+
25
+ export interface EventTrigger<T> extends Trigger<T> {
26
+ readonly kind: "event";
27
+ readonly event: EventDefinition<T>;
28
+ }
29
+
30
+ /** A Trigger contributed by a Connector (ADR-0013) — produced by
31
+ * `<connector>.triggers.<name>(params)`, never constructed by hand. */
32
+ export interface ConnectorTrigger<T> extends Trigger<T> {
33
+ readonly kind: "connector";
34
+ readonly connectorId: string;
35
+ readonly triggerId: string;
36
+ readonly params: Record<string, unknown>;
37
+ /** Names the binding in the automation's `connections` map; auto-bound when unambiguous. */
38
+ readonly connectionKey?: string;
39
+ /** Poll cadence override (reserved `every` param) — clamped to the connector's floor. */
40
+ readonly every?: string;
41
+ }
42
+
43
+ /** Inbound webhook. Route is auto-derived from project + automation id. Responds 202 with a
44
+ * runId by default; `respond: "sync"` waits for and returns the handler's output. `input`
45
+ * validates the body; raw headers / bytes (for HMAC) arrive on ctx.request. */
46
+ export function onWebhook<T = unknown>(opts?: {
47
+ input?: StandardSchemaV1<unknown, T>;
48
+ respond?: "async" | "sync";
49
+ }): WebhookTrigger<T> {
50
+ const trigger: WebhookTrigger<T> = {
51
+ kind: "webhook",
52
+ respond: opts?.respond ?? "async",
53
+ ...(opts?.input !== undefined ? { input: opts.input } : {}),
54
+ };
55
+ return Object.freeze(trigger);
56
+ }
57
+
58
+ // Light shape check only — strict parsing (ranges, day-of-week names, tz) happens at build.
59
+ const CRON_SHAPE = /^\s*\S+(\s+\S+){4,5}\s*$/;
60
+
61
+ export function onSchedule(opts: { cron: string; tz?: string }): ScheduleTrigger {
62
+ if (typeof opts?.cron !== "string" || !CRON_SHAPE.test(opts.cron)) {
63
+ throw new TypeError(
64
+ `onSchedule: cron "${String(opts?.cron)}" — expected 5 (or 6) space-separated fields, e.g. "0 9 * * *"`,
65
+ );
66
+ }
67
+ return Object.freeze({
68
+ kind: "schedule" as const,
69
+ cron: opts.cron.trim(),
70
+ ...(opts.tz !== undefined ? { tz: opts.tz } : {}),
71
+ });
72
+ }
73
+
74
+ export function onEvent<T>(event: EventDefinition<T>): EventTrigger<T> {
75
+ if (typeof event?.name !== "string" || event.schema === undefined) {
76
+ throw new TypeError("onEvent: pass an EventDefinition created by defineEvent(...)");
77
+ }
78
+ return Object.freeze({ kind: "event" as const, event });
79
+ }