feat(platform-service): fleet data model + container registration (P2 foundation)
Adds the agent-gigafactory fleet data model (modules/fleet/types.ts): Zod schemas as the source of truth with inferred types (no `any`) for the 7 durable containers — FleetJobDoc, FleetRunDoc, FleetLeaseDoc, FleetFactoryDoc, FleetProfileDoc, FleetEventDoc, FleetArtifactDoc — each carrying productId. Lifecycle stages mirror the agent-queue gigafactory spec (queued|blocked|assigned|building|review|testing| shipped|failed|dead_letter). Registers fleet_* containers with their partition keys (/productId for jobs/factories/profiles, /jobId for runs/leases/events/artifacts).
This commit is contained in:
parent
1846201364
commit
721d3fcb48
@ -187,6 +187,14 @@ const CONTAINER_DEFS: Record<string, ContainerConfig> = {
|
|||||||
// i18n (P3)
|
// i18n (P3)
|
||||||
translations: { partitionKeyPath: '/locale' },
|
translations: { partitionKeyPath: '/locale' },
|
||||||
i18n_locales: { partitionKeyPath: '/locale' },
|
i18n_locales: { partitionKeyPath: '/locale' },
|
||||||
|
// Agent Gigafactory — fleet coordinator (see modules/fleet/README.md)
|
||||||
|
fleet_jobs: { partitionKeyPath: '/productId' },
|
||||||
|
fleet_runs: { partitionKeyPath: '/jobId' },
|
||||||
|
fleet_leases: { partitionKeyPath: '/jobId' },
|
||||||
|
fleet_factories: { partitionKeyPath: '/productId' },
|
||||||
|
fleet_profiles: { partitionKeyPath: '/productId' },
|
||||||
|
fleet_events: { partitionKeyPath: '/jobId' },
|
||||||
|
fleet_artifacts: { partitionKeyPath: '/jobId' },
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function initCosmosIfNeeded(): Promise<void> {
|
export async function initCosmosIfNeeded(): Promise<void> {
|
||||||
|
|||||||
219
services/platform-service/src/modules/fleet/types.test.ts
Normal file
219
services/platform-service/src/modules/fleet/types.test.ts
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
/**
|
||||||
|
* Fleet schema validation — valid docs pass; missing productId / bad enum fail.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import {
|
||||||
|
FleetJobDocSchema,
|
||||||
|
FleetRunDocSchema,
|
||||||
|
FleetLeaseDocSchema,
|
||||||
|
FleetFactoryDocSchema,
|
||||||
|
FleetProfileDocSchema,
|
||||||
|
FleetEventDocSchema,
|
||||||
|
FleetArtifactDocSchema,
|
||||||
|
SubmitJobSchema,
|
||||||
|
PatchJobSchema,
|
||||||
|
ClaimSchema,
|
||||||
|
FLEET_STAGES,
|
||||||
|
FLEET_PRIORITIES,
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
|
const now = '2026-05-30T00:00:00.000Z';
|
||||||
|
|
||||||
|
const validJob = {
|
||||||
|
id: 'fjob_1',
|
||||||
|
productId: 'lysnrai',
|
||||||
|
stage: 'queued',
|
||||||
|
idempotencyKey: 'task-1',
|
||||||
|
contentHash: 'abc',
|
||||||
|
bodyMd: '# task',
|
||||||
|
manifestSnapshot: {
|
||||||
|
priority: 'medium',
|
||||||
|
capabilities: [],
|
||||||
|
prefersEngine: [],
|
||||||
|
allowedScope: [],
|
||||||
|
deps: [],
|
||||||
|
},
|
||||||
|
priority: 'medium',
|
||||||
|
priorityOrder: 2,
|
||||||
|
capabilities: [],
|
||||||
|
deps: [],
|
||||||
|
kind: 'leaf',
|
||||||
|
attempts: 0,
|
||||||
|
leaseEpoch: 0,
|
||||||
|
rev: 0,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('FleetJobDocSchema', () => {
|
||||||
|
it('accepts a valid job', () => {
|
||||||
|
expect(FleetJobDocSchema.safeParse(validJob).success).toBe(true);
|
||||||
|
});
|
||||||
|
it('rejects a missing productId', () => {
|
||||||
|
const { productId: _omit, ...bad } = validJob;
|
||||||
|
expect(FleetJobDocSchema.safeParse(bad).success).toBe(false);
|
||||||
|
});
|
||||||
|
it('rejects an empty productId', () => {
|
||||||
|
expect(FleetJobDocSchema.safeParse({ ...validJob, productId: '' }).success).toBe(false);
|
||||||
|
});
|
||||||
|
it('rejects an invalid stage enum', () => {
|
||||||
|
expect(FleetJobDocSchema.safeParse({ ...validJob, stage: 'done' }).success).toBe(false);
|
||||||
|
});
|
||||||
|
it('rejects an invalid priority enum', () => {
|
||||||
|
expect(FleetJobDocSchema.safeParse({ ...validJob, priority: 'urgent' }).success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('FleetRunDocSchema', () => {
|
||||||
|
const validRun = {
|
||||||
|
id: 'fjob_1:run:1',
|
||||||
|
productId: 'lysnrai',
|
||||||
|
jobId: 'fjob_1',
|
||||||
|
attempt: 1,
|
||||||
|
engine: 'devin',
|
||||||
|
startedAt: now,
|
||||||
|
insights: { tokensIn: 10, tokensOut: 5 },
|
||||||
|
};
|
||||||
|
it('accepts a valid run', () => {
|
||||||
|
expect(FleetRunDocSchema.safeParse(validRun).success).toBe(true);
|
||||||
|
});
|
||||||
|
it('rejects missing productId', () => {
|
||||||
|
const { productId: _o, ...bad } = validRun;
|
||||||
|
expect(FleetRunDocSchema.safeParse(bad).success).toBe(false);
|
||||||
|
});
|
||||||
|
it('rejects attempt <= 0', () => {
|
||||||
|
expect(FleetRunDocSchema.safeParse({ ...validRun, attempt: 0 }).success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('FleetLeaseDocSchema', () => {
|
||||||
|
const validLease = {
|
||||||
|
id: 'fjob_1',
|
||||||
|
productId: 'lysnrai',
|
||||||
|
jobId: 'fjob_1',
|
||||||
|
leaseEpoch: 1,
|
||||||
|
renewals: 0,
|
||||||
|
status: 'held',
|
||||||
|
rev: 0,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
it('accepts a valid lease', () => {
|
||||||
|
expect(FleetLeaseDocSchema.safeParse(validLease).success).toBe(true);
|
||||||
|
});
|
||||||
|
it('rejects an invalid status', () => {
|
||||||
|
expect(FleetLeaseDocSchema.safeParse({ ...validLease, status: 'open' }).success).toBe(false);
|
||||||
|
});
|
||||||
|
it('rejects missing productId', () => {
|
||||||
|
const { productId: _o, ...bad } = validLease;
|
||||||
|
expect(FleetLeaseDocSchema.safeParse(bad).success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('FleetFactoryDocSchema', () => {
|
||||||
|
const validFactory = {
|
||||||
|
id: 'fac_1',
|
||||||
|
productId: 'lysnrai',
|
||||||
|
factoryId: 'fac_1',
|
||||||
|
descriptor: { os: 'mac' },
|
||||||
|
capabilities: ['os:mac'],
|
||||||
|
health: 'ok',
|
||||||
|
load: 0,
|
||||||
|
seatLimit: 2,
|
||||||
|
lastHeartbeatAt: now,
|
||||||
|
};
|
||||||
|
it('accepts a valid factory', () => {
|
||||||
|
expect(FleetFactoryDocSchema.safeParse(validFactory).success).toBe(true);
|
||||||
|
});
|
||||||
|
it('rejects invalid health enum', () => {
|
||||||
|
expect(FleetFactoryDocSchema.safeParse({ ...validFactory, health: 'sick' }).success).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('FleetProfileDocSchema / FleetEventDocSchema / FleetArtifactDocSchema', () => {
|
||||||
|
it('accepts a valid profile and rejects missing productId', () => {
|
||||||
|
const valid = {
|
||||||
|
id: 'prof_1',
|
||||||
|
productId: 'lysnrai',
|
||||||
|
name: 'backend',
|
||||||
|
version: 1,
|
||||||
|
snapshot: {},
|
||||||
|
createdAt: now,
|
||||||
|
};
|
||||||
|
expect(FleetProfileDocSchema.safeParse(valid).success).toBe(true);
|
||||||
|
const { productId: _o, ...bad } = valid;
|
||||||
|
expect(FleetProfileDocSchema.safeParse(bad).success).toBe(false);
|
||||||
|
});
|
||||||
|
it('accepts a valid event and rejects missing type', () => {
|
||||||
|
const valid = {
|
||||||
|
id: 'fjob_1:evt:0',
|
||||||
|
productId: 'lysnrai',
|
||||||
|
jobId: 'fjob_1',
|
||||||
|
seq: 0,
|
||||||
|
type: 'submitted',
|
||||||
|
at: now,
|
||||||
|
data: {},
|
||||||
|
};
|
||||||
|
expect(FleetEventDocSchema.safeParse(valid).success).toBe(true);
|
||||||
|
const { type: _t, ...bad } = valid;
|
||||||
|
expect(FleetEventDocSchema.safeParse(bad).success).toBe(false);
|
||||||
|
});
|
||||||
|
it('accepts a valid artifact and rejects missing blobUrl', () => {
|
||||||
|
const valid = {
|
||||||
|
id: 'art_1',
|
||||||
|
productId: 'lysnrai',
|
||||||
|
jobId: 'fjob_1',
|
||||||
|
kind: 'coverage',
|
||||||
|
blobUrl: 'https://b/x',
|
||||||
|
createdAt: now,
|
||||||
|
};
|
||||||
|
expect(FleetArtifactDocSchema.safeParse(valid).success).toBe(true);
|
||||||
|
const { blobUrl: _b, ...bad } = valid;
|
||||||
|
expect(FleetArtifactDocSchema.safeParse(bad).success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('request schemas', () => {
|
||||||
|
it('SubmitJobSchema applies defaults', () => {
|
||||||
|
const parsed = SubmitJobSchema.safeParse({ idempotencyKey: 'k', bodyMd: '# do' });
|
||||||
|
expect(parsed.success).toBe(true);
|
||||||
|
if (parsed.success) {
|
||||||
|
expect(parsed.data.priority).toBe('medium');
|
||||||
|
expect(parsed.data.kind).toBe('leaf');
|
||||||
|
expect(parsed.data.deps).toEqual([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('SubmitJobSchema rejects empty bodyMd / idempotencyKey', () => {
|
||||||
|
expect(SubmitJobSchema.safeParse({ idempotencyKey: '', bodyMd: 'x' }).success).toBe(false);
|
||||||
|
expect(SubmitJobSchema.safeParse({ idempotencyKey: 'k', bodyMd: '' }).success).toBe(false);
|
||||||
|
});
|
||||||
|
it('PatchJobSchema requires leaseEpoch', () => {
|
||||||
|
expect(PatchJobSchema.safeParse({ stage: 'building' }).success).toBe(false);
|
||||||
|
expect(PatchJobSchema.safeParse({ leaseEpoch: 1, stage: 'building' }).success).toBe(true);
|
||||||
|
});
|
||||||
|
it('ClaimSchema requires factoryId', () => {
|
||||||
|
expect(ClaimSchema.safeParse({ capabilities: [] }).success).toBe(false);
|
||||||
|
expect(ClaimSchema.safeParse({ factoryId: 'fac_1' }).success).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('enum constants', () => {
|
||||||
|
it('stages match the agent-queue lifecycle', () => {
|
||||||
|
expect(FLEET_STAGES).toEqual([
|
||||||
|
'queued',
|
||||||
|
'blocked',
|
||||||
|
'assigned',
|
||||||
|
'building',
|
||||||
|
'review',
|
||||||
|
'testing',
|
||||||
|
'shipped',
|
||||||
|
'failed',
|
||||||
|
'dead_letter',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('priorities', () => {
|
||||||
|
expect(FLEET_PRIORITIES).toEqual(['critical', 'high', 'medium', 'low']);
|
||||||
|
});
|
||||||
|
});
|
||||||
317
services/platform-service/src/modules/fleet/types.ts
Normal file
317
services/platform-service/src/modules/fleet/types.ts
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
/**
|
||||||
|
* Fleet module — types for the agent-gigafactory coordinator (Phase 2 foundation).
|
||||||
|
*
|
||||||
|
* Durable data model for distributed agent jobs: jobs, runs, leases, factories,
|
||||||
|
* profiles, events, and artifacts. Product-agnostic — every document carries a
|
||||||
|
* `productId`. Zod schemas are the source of truth; doc/input types are inferred
|
||||||
|
* from them (no `any`).
|
||||||
|
*
|
||||||
|
* Field names + lifecycle mirror the agent-queue gigafactory spec
|
||||||
|
* (../learning_ai_devops_tools/agent-queue/docs/GIGAFACTORY_ROADMAP.md §4/§7/§8/§13).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
// ── Enums ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Canonical job lifecycle (§11). `blocked` = unmet deps; `dead_letter` = retries exhausted. */
|
||||||
|
export const FLEET_STAGES = [
|
||||||
|
'queued',
|
||||||
|
'blocked',
|
||||||
|
'assigned',
|
||||||
|
'building',
|
||||||
|
'review',
|
||||||
|
'testing',
|
||||||
|
'shipped',
|
||||||
|
'failed',
|
||||||
|
'dead_letter',
|
||||||
|
] as const;
|
||||||
|
export type FleetStage = (typeof FLEET_STAGES)[number];
|
||||||
|
|
||||||
|
/** Stages from which a worker may still progress / that count as "in flight". */
|
||||||
|
export const ACTIVE_STAGES: readonly FleetStage[] = ['assigned', 'building', 'review', 'testing'];
|
||||||
|
/** Stages that satisfy a hard dependency / a soft dependency. */
|
||||||
|
export const DEP_DONE_HARD: readonly FleetStage[] = ['shipped'];
|
||||||
|
export const DEP_DONE_SOFT: readonly FleetStage[] = ['shipped', 'testing'];
|
||||||
|
|
||||||
|
export const FLEET_PRIORITIES = ['critical', 'high', 'medium', 'low'] as const;
|
||||||
|
export type FleetPriority = (typeof FLEET_PRIORITIES)[number];
|
||||||
|
export const PRIORITY_ORDER: Record<FleetPriority, number> = {
|
||||||
|
critical: 0,
|
||||||
|
high: 1,
|
||||||
|
medium: 2,
|
||||||
|
low: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FLEET_ENGINE_CLASSES = ['agentic-coder', 'chat-coder', 'review-only'] as const;
|
||||||
|
export type FleetEngineClass = (typeof FLEET_ENGINE_CLASSES)[number];
|
||||||
|
|
||||||
|
export const DEPS_MODES = ['hard', 'soft'] as const;
|
||||||
|
export type DepsMode = (typeof DEPS_MODES)[number];
|
||||||
|
|
||||||
|
/** Failure classes a `retry.on` may name (mirrors agent-queue). */
|
||||||
|
export const RETRY_CLASSES = ['timeout', 'verify_failed', 'crash', 'agent_error'] as const;
|
||||||
|
export type RetryClass = (typeof RETRY_CLASSES)[number];
|
||||||
|
|
||||||
|
/** Terminal run results recorded on a FleetRunDoc. */
|
||||||
|
export const RUN_RESULTS = [
|
||||||
|
'review',
|
||||||
|
'testing',
|
||||||
|
'shipped',
|
||||||
|
'failed',
|
||||||
|
'timeout',
|
||||||
|
'verify_failed',
|
||||||
|
'capability_mismatch',
|
||||||
|
'no_engine',
|
||||||
|
'retries_exhausted',
|
||||||
|
] as const;
|
||||||
|
export type RunResult = (typeof RUN_RESULTS)[number];
|
||||||
|
|
||||||
|
export const FACTORY_HEALTH = ['ok', 'degraded', 'down'] as const;
|
||||||
|
export type FactoryHealth = (typeof FACTORY_HEALTH)[number];
|
||||||
|
|
||||||
|
export const LEASE_STATUS = ['held', 'expired', 'released'] as const;
|
||||||
|
export type LeaseStatus = (typeof LEASE_STATUS)[number];
|
||||||
|
|
||||||
|
export const JOB_KINDS = ['leaf', 'composite'] as const;
|
||||||
|
export type JobKind = (typeof JOB_KINDS)[number];
|
||||||
|
|
||||||
|
// ── Shared value objects ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const CheckpointSchema = z.object({
|
||||||
|
wipBranch: z.string(),
|
||||||
|
wipBase: z.string().optional(),
|
||||||
|
wipCommit: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BudgetSchema = z.object({
|
||||||
|
usd: z.number().nonnegative().optional(),
|
||||||
|
tokens: z.number().nonnegative().optional(),
|
||||||
|
wall: z.number().nonnegative().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const RetryPolicySchema = z.object({
|
||||||
|
max: z.number().int().nonnegative(),
|
||||||
|
backoff: z.string().optional(),
|
||||||
|
on: z.array(z.enum(RETRY_CLASSES)).default([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ManifestSnapshotSchema = z.object({
|
||||||
|
priority: z.enum(FLEET_PRIORITIES).default('medium'),
|
||||||
|
capabilities: z.array(z.string()).default([]),
|
||||||
|
engineClass: z.enum(FLEET_ENGINE_CLASSES).optional(),
|
||||||
|
profile: z.string().optional(),
|
||||||
|
prefersEngine: z.array(z.string()).default([]),
|
||||||
|
allowedScope: z.array(z.string()).default([]),
|
||||||
|
deps: z.array(z.string()).default([]),
|
||||||
|
depsMode: z.enum(DEPS_MODES).optional(),
|
||||||
|
budget: BudgetSchema.optional(),
|
||||||
|
retry: RetryPolicySchema.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const InsightsSchema = z.object({
|
||||||
|
model: z.string().optional(),
|
||||||
|
tokensIn: z.number().optional(),
|
||||||
|
tokensOut: z.number().optional(),
|
||||||
|
tokensCached: z.number().optional(),
|
||||||
|
costUsd: z.number().optional(),
|
||||||
|
estimated: z.boolean().optional(),
|
||||||
|
turns: z.number().optional(),
|
||||||
|
toolCalls: z.number().optional(),
|
||||||
|
filesChanged: z.number().optional(),
|
||||||
|
linesAdded: z.number().optional(),
|
||||||
|
linesDeleted: z.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Container documents ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FleetJobDoc — the durable job (pk `/productId`).
|
||||||
|
* `rev` is the optimistic-concurrency token used by the coordinator's atomic
|
||||||
|
* claim + fenced transitions (maps to Cosmos `_etag` / If-Match in production;
|
||||||
|
* see repository.revUpdate).
|
||||||
|
*/
|
||||||
|
export const FleetJobDocSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
productId: z.string().min(1),
|
||||||
|
stage: z.enum(FLEET_STAGES),
|
||||||
|
idempotencyKey: z.string().min(1),
|
||||||
|
contentHash: z.string(),
|
||||||
|
bodyMd: z.string(),
|
||||||
|
manifestSnapshot: ManifestSnapshotSchema,
|
||||||
|
priority: z.enum(FLEET_PRIORITIES),
|
||||||
|
priorityOrder: z.number().int(),
|
||||||
|
capabilities: z.array(z.string()).default([]),
|
||||||
|
engineClass: z.enum(FLEET_ENGINE_CLASSES).optional(),
|
||||||
|
profile: z.string().optional(),
|
||||||
|
deps: z.array(z.string()).default([]),
|
||||||
|
depsMode: z.enum(DEPS_MODES).optional(),
|
||||||
|
budget: BudgetSchema.optional(),
|
||||||
|
retry: RetryPolicySchema.optional(),
|
||||||
|
kind: z.enum(JOB_KINDS).default('leaf'),
|
||||||
|
parentId: z.string().optional(),
|
||||||
|
trackerItemId: z.string().optional(),
|
||||||
|
checkpoint: CheckpointSchema.optional(),
|
||||||
|
attempts: z.number().int().nonnegative().default(0),
|
||||||
|
leaseEpoch: z.number().int().nonnegative().default(0),
|
||||||
|
rev: z.number().int().nonnegative().default(0),
|
||||||
|
blockedReason: z.string().optional(),
|
||||||
|
createdAt: z.string(),
|
||||||
|
updatedAt: z.string(),
|
||||||
|
});
|
||||||
|
export type FleetJobDoc = z.infer<typeof FleetJobDocSchema>;
|
||||||
|
|
||||||
|
/** FleetRunDoc — one execution attempt of a job (pk `/jobId`). */
|
||||||
|
export const FleetRunDocSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
productId: z.string().min(1),
|
||||||
|
jobId: z.string().min(1),
|
||||||
|
attempt: z.number().int().positive(),
|
||||||
|
factoryId: z.string().optional(),
|
||||||
|
engine: z.string(),
|
||||||
|
profileSnapshot: z.record(z.string(), z.unknown()).optional(),
|
||||||
|
startedAt: z.string(),
|
||||||
|
endedAt: z.string().optional(),
|
||||||
|
exit: z.number().int().optional(),
|
||||||
|
verifyResult: z.enum(['pass', 'fail']).optional(),
|
||||||
|
result: z.enum(RUN_RESULTS).optional(),
|
||||||
|
insights: InsightsSchema.default({}),
|
||||||
|
});
|
||||||
|
export type FleetRunDoc = z.infer<typeof FleetRunDocSchema>;
|
||||||
|
|
||||||
|
/** FleetLeaseDoc — the single-holder lease for a job (pk `/jobId`). */
|
||||||
|
export const FleetLeaseDocSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
productId: z.string().min(1),
|
||||||
|
jobId: z.string().min(1),
|
||||||
|
holderFactoryId: z.string().optional(),
|
||||||
|
expiresAt: z.string().optional(),
|
||||||
|
leaseEpoch: z.number().int().nonnegative().default(0),
|
||||||
|
renewals: z.number().int().nonnegative().default(0),
|
||||||
|
status: z.enum(LEASE_STATUS).default('held'),
|
||||||
|
rev: z.number().int().nonnegative().default(0),
|
||||||
|
updatedAt: z.string(),
|
||||||
|
});
|
||||||
|
export type FleetLeaseDoc = z.infer<typeof FleetLeaseDocSchema>;
|
||||||
|
|
||||||
|
/** FleetFactoryDoc — a registered worker host (pk `/productId`). */
|
||||||
|
export const FleetFactoryDocSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
productId: z.string().min(1),
|
||||||
|
factoryId: z.string().min(1),
|
||||||
|
descriptor: z.record(z.string(), z.unknown()).default({}),
|
||||||
|
capabilities: z.array(z.string()).default([]),
|
||||||
|
health: z.enum(FACTORY_HEALTH).default('ok'),
|
||||||
|
load: z.number().nonnegative().default(0),
|
||||||
|
seatLimit: z.number().int().positive().default(1),
|
||||||
|
lastHeartbeatAt: z.string(),
|
||||||
|
});
|
||||||
|
export type FleetFactoryDoc = z.infer<typeof FleetFactoryDocSchema>;
|
||||||
|
|
||||||
|
/** FleetProfileDoc — an immutable, versioned profile snapshot (pk `/productId`). */
|
||||||
|
export const FleetProfileDocSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
productId: z.string().min(1),
|
||||||
|
name: z.string().min(1),
|
||||||
|
version: z.number().int().positive(),
|
||||||
|
snapshot: z.record(z.string(), z.unknown()),
|
||||||
|
createdAt: z.string(),
|
||||||
|
});
|
||||||
|
export type FleetProfileDoc = z.infer<typeof FleetProfileDocSchema>;
|
||||||
|
|
||||||
|
/** FleetEventDoc — append-only audit/event stream entry (pk `/jobId`). */
|
||||||
|
export const FleetEventDocSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
productId: z.string().min(1),
|
||||||
|
jobId: z.string().min(1),
|
||||||
|
seq: z.number().int().nonnegative(),
|
||||||
|
type: z.string().min(1),
|
||||||
|
at: z.string(),
|
||||||
|
actor: z.string().optional(),
|
||||||
|
data: z.record(z.string(), z.unknown()).default({}),
|
||||||
|
});
|
||||||
|
export type FleetEventDoc = z.infer<typeof FleetEventDocSchema>;
|
||||||
|
|
||||||
|
/** FleetArtifactDoc — pointer to a blob-stored artifact (pk `/jobId`). No inline logs. */
|
||||||
|
export const FleetArtifactDocSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
productId: z.string().min(1),
|
||||||
|
jobId: z.string().min(1),
|
||||||
|
runId: z.string().optional(),
|
||||||
|
kind: z.string().min(1),
|
||||||
|
blobUrl: z.string().min(1),
|
||||||
|
sizeBytes: z.number().int().nonnegative().optional(),
|
||||||
|
createdAt: z.string(),
|
||||||
|
});
|
||||||
|
export type FleetArtifactDoc = z.infer<typeof FleetArtifactDocSchema>;
|
||||||
|
|
||||||
|
// ── API request schemas (routes) ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const SubmitJobSchema = z.object({
|
||||||
|
productId: z.string().min(1).optional(),
|
||||||
|
idempotencyKey: z.string().min(1),
|
||||||
|
bodyMd: z.string().min(1),
|
||||||
|
priority: z.enum(FLEET_PRIORITIES).default('medium'),
|
||||||
|
capabilities: z.array(z.string()).default([]),
|
||||||
|
engineClass: z.enum(FLEET_ENGINE_CLASSES).optional(),
|
||||||
|
profile: z.string().optional(),
|
||||||
|
prefersEngine: z.array(z.string()).default([]),
|
||||||
|
allowedScope: z.array(z.string()).default([]),
|
||||||
|
deps: z.array(z.string()).default([]),
|
||||||
|
depsMode: z.enum(DEPS_MODES).optional(),
|
||||||
|
budget: BudgetSchema.optional(),
|
||||||
|
retry: RetryPolicySchema.optional(),
|
||||||
|
kind: z.enum(JOB_KINDS).default('leaf'),
|
||||||
|
parentId: z.string().optional(),
|
||||||
|
trackerItemId: z.string().optional(),
|
||||||
|
});
|
||||||
|
export type SubmitJobInput = z.infer<typeof SubmitJobSchema>;
|
||||||
|
|
||||||
|
export const ListJobsQuerySchema = z.object({
|
||||||
|
productId: z.string().optional(),
|
||||||
|
stage: z.enum(FLEET_STAGES).optional(),
|
||||||
|
idempotencyKey: z.string().optional(),
|
||||||
|
limit: z.coerce.number().int().min(1).max(200).default(50),
|
||||||
|
offset: z.coerce.number().int().min(0).default(0),
|
||||||
|
});
|
||||||
|
export type ListJobsQuery = z.infer<typeof ListJobsQuerySchema>;
|
||||||
|
|
||||||
|
/** Fenced state transition — worker MUST carry its leaseEpoch. */
|
||||||
|
export const PatchJobSchema = z.object({
|
||||||
|
leaseEpoch: z.number().int().nonnegative(),
|
||||||
|
stage: z.enum(FLEET_STAGES).optional(),
|
||||||
|
checkpoint: CheckpointSchema.optional(),
|
||||||
|
blockedReason: z.string().optional(),
|
||||||
|
});
|
||||||
|
export type PatchJobInput = z.infer<typeof PatchJobSchema>;
|
||||||
|
|
||||||
|
export const ClaimSchema = z.object({
|
||||||
|
productId: z.string().min(1).optional(),
|
||||||
|
factoryId: z.string().min(1),
|
||||||
|
capabilities: z.array(z.string()).default([]),
|
||||||
|
leaseSeconds: z.number().int().positive().max(86400).default(900),
|
||||||
|
});
|
||||||
|
export type ClaimInput = z.infer<typeof ClaimSchema>;
|
||||||
|
|
||||||
|
export const RenewLeaseSchema = z.object({
|
||||||
|
leaseEpoch: z.number().int().nonnegative(),
|
||||||
|
leaseSeconds: z.number().int().positive().max(86400).default(900),
|
||||||
|
});
|
||||||
|
export type RenewLeaseInput = z.infer<typeof RenewLeaseSchema>;
|
||||||
|
|
||||||
|
export const ReleaseLeaseSchema = z.object({
|
||||||
|
leaseEpoch: z.number().int().nonnegative(),
|
||||||
|
stage: z.enum(FLEET_STAGES).optional(),
|
||||||
|
});
|
||||||
|
export type ReleaseLeaseInput = z.infer<typeof ReleaseLeaseSchema>;
|
||||||
|
|
||||||
|
export const HeartbeatSchema = z.object({
|
||||||
|
productId: z.string().min(1).optional(),
|
||||||
|
factoryId: z.string().min(1),
|
||||||
|
descriptor: z.record(z.string(), z.unknown()).optional(),
|
||||||
|
capabilities: z.array(z.string()).optional(),
|
||||||
|
health: z.enum(FACTORY_HEALTH).optional(),
|
||||||
|
load: z.number().nonnegative().optional(),
|
||||||
|
seatLimit: z.number().int().positive().optional(),
|
||||||
|
});
|
||||||
|
export type HeartbeatInput = z.infer<typeof HeartbeatSchema>;
|
||||||
Loading…
Reference in New Issue
Block a user