blob: ef299bb3bffc497b89cc4d3e0ef0e62d91a90f59 [file]
import type { BackendKind } from './session.js';
import type { CollaborationMode } from './collaboration.js';
import type { OrchestrationMode } from './orchestration.js';
import type { ThinkingLevel } from './model-thinking.js';
const UTF8 = new TextEncoder();
// Preserve the embedded model schema's code-unit limits while giving every
// boundary an explicit UTF-8 ceiling. TextEncoder needs at most three bytes per
// JavaScript code unit, including replacement encoding for lone surrogates.
export const AUTOMATION_NAME_MAX_CODE_UNITS = 100;
export const AUTOMATION_NAME_MAX_BYTES = 300;
export const AUTOMATION_PROMPT_MAX_CODE_UNITS = 2_000;
export const AUTOMATION_PROMPT_MAX_BYTES = 6_000;
export const AUTOMATION_CRON_EXPRESSION_MAX_CODE_UNITS = 100;
export const AUTOMATION_CRON_EXPRESSION_MAX_BYTES = 300;
export const AUTOMATION_LAST_ERROR_MAX_CODE_UNITS = 2_000;
export const AUTOMATION_LAST_ERROR_MAX_BYTES = 6_000;
export interface AutomationTextLimit {
readonly maxCodeUnits: number;
readonly maxBytes: number;
}
export const AUTOMATION_NAME_LIMIT: AutomationTextLimit = Object.freeze({
maxCodeUnits: AUTOMATION_NAME_MAX_CODE_UNITS,
maxBytes: AUTOMATION_NAME_MAX_BYTES,
});
export const AUTOMATION_PROMPT_LIMIT: AutomationTextLimit = Object.freeze({
maxCodeUnits: AUTOMATION_PROMPT_MAX_CODE_UNITS,
maxBytes: AUTOMATION_PROMPT_MAX_BYTES,
});
export const AUTOMATION_CRON_EXPRESSION_LIMIT: AutomationTextLimit = Object.freeze({
maxCodeUnits: AUTOMATION_CRON_EXPRESSION_MAX_CODE_UNITS,
maxBytes: AUTOMATION_CRON_EXPRESSION_MAX_BYTES,
});
export const AUTOMATION_LAST_ERROR_LIMIT: AutomationTextLimit = Object.freeze({
maxCodeUnits: AUTOMATION_LAST_ERROR_MAX_CODE_UNITS,
maxBytes: AUTOMATION_LAST_ERROR_MAX_BYTES,
});
export function isAutomationTextWithinLimit(
value: unknown,
limit: AutomationTextLimit,
options: { readonly nonblank?: boolean } = {},
): value is string {
return (
typeof value === 'string' &&
value.length > 0 &&
value.length <= limit.maxCodeUnits &&
UTF8.encode(value).byteLength <= limit.maxBytes &&
(options.nonblank !== true || value.trim().length > 0)
);
}
export function truncateAutomationText(value: string, limit: AutomationTextLimit): string {
if (value.length <= limit.maxCodeUnits && UTF8.encode(value).byteLength <= limit.maxBytes) {
return value;
}
let result = '';
let bytes = 0;
for (const character of value) {
const nextBytes = UTF8.encode(character).byteLength;
if (
result.length + character.length > limit.maxCodeUnits ||
bytes + nextBytes > limit.maxBytes
) {
break;
}
result += character;
bytes += nextBytes;
}
return result;
}
export type AutomationKind = 'heartbeat' | 'cron';
export type AutomationStatus = 'active' | 'paused' | 'completed' | 'expired';
export type AutomationSchedule =
| { type: 'cron'; expression: string }
| { type: 'interval'; seconds: number }
| { type: 'once'; delaySeconds: number };
/** Frozen execution settings used by durable cron fires after their creator changes. */
export interface AutomationExecutionTemplate {
readonly cwd: string;
readonly projectId?: string | null;
readonly backend: BackendKind;
readonly llmConnectionSlug: string;
readonly model: string;
readonly thinkingLevel?: ThinkingLevel;
readonly collaborationMode: CollaborationMode;
readonly orchestrationMode: OrchestrationMode;
}
export interface AutomationDefinition {
id: string;
kind: AutomationKind;
name: string;
status: AutomationStatus;
prompt: string;
sessionId: string;
schedule: AutomationSchedule;
createdAt: number;
updatedAt: number;
nextFireAt: number | null;
lastFireAt: number | null;
lastRunId: string | null;
fireCount: number;
maxFires: number | null;
expiresAt: number | null;
lastError: string | null;
consecutiveFailures: number;
/** Cron definitions are globally visible and survive the creating Client. */
durable?: boolean;
deferredFireCount?: number;
/** Present for Host-created cron definitions; legacy definitions acquire it before firing. */
execution?: AutomationExecutionTemplate;
}
/** Durable execution intent removed atomically when its canonical Run settles. */
export interface AutomationPendingFire {
readonly id: string;
readonly automationId: string;
readonly automationKind: AutomationKind;
readonly automationName: string;
readonly prompt: string;
readonly scheduledFor: number;
readonly targetSessionId: string;
readonly turnId: string;
readonly runId: string;
readonly userMessageId: string;
readonly status: 'admitted' | 'running';
readonly admittedAt: number;
readonly updatedAt: number;
readonly startedAt?: number;
readonly execution?: AutomationExecutionTemplate;
}
export interface AutomationAuthoritySnapshot {
readonly revision: number;
readonly automations: readonly AutomationDefinition[];
readonly pendingFires: readonly AutomationPendingFire[];
}