Стройте control plane независимо от model provider
Business state, budgets, permissions и acceptance не должны исчезать при смене SDK. Provider adapter отвечает за model loop, а приложение отвечает за систему. Полезно разделить три слоя: domain определяет work items, artifacts и acceptance; control plane управляет graph, budget, state, identity и trace; model adapter превращает provider-specific response, tool call или handoff в ваши typed events.
- Domain: task и outcome
- Control plane: graph, policy, state
- Adapter: SDK run и tools
- Provider: model service
- Evidence: external gate
Провайдер-независимость держится на узком интерфейсе handler - модель за ним взаимозаменяема.
export interface AgentResult {
readonly summary: string;
readonly artifacts: readonly ArtifactDraft[];
readonly evidence: readonly Evidence[];
readonly usage: Usage;
}
export interface AgentContext {
readonly runId: string;
readonly task: WorkItem;
readonly inputArtifacts: readonly Artifact[];
readonly attempt: number;
}
export interface AgentHandler {
readonly contract: AgentContract;
run(context: AgentContext): Promise<AgentResult>;
}
export interface BudgetLimits {
readonly maxCalls: number;
readonly maxTokens: number;
readonly maxCostUnits: number;
readonly maxSimulatedLatencyMs: number;
}
export interface BudgetSnapshot extends BudgetLimits {
readonly usedCalls: number;А сам control plane валидирует граф и контракты до запуска и владеет исполнением.
export class Orchestrator {
readonly #tasks: readonly WorkItem[];
readonly #handlers: ReadonlyMap<string, AgentHandler>;
readonly #limits: BudgetLimits;
readonly #maxConcurrency: number;
public constructor(input: {
readonly tasks: readonly WorkItem[];
readonly handlers: readonly AgentHandler[];
readonly limits: BudgetLimits;
readonly maxConcurrency: number;
}) {
validateWorkGraph(input.tasks);
validateHandlers(input.handlers);
if (!Number.isInteger(input.maxConcurrency) || input.maxConcurrency < 1) {
throw new Error("maxConcurrency must be a positive integer");
}
this.#tasks = input.tasks;
this.#handlers = new Map(input.handlers.map((handler) => [handler.contract.id, handler]));
this.#limits = input.limits;
this.#maxConcurrency = input.maxConcurrency;
}
public async run(runId: string): Promise<RunReport> {
const status: Record<string, TaskStatus> = createInitialStatus(this.#tasks);
const budget = new BudgetLedger(this.#limits);
const eventLog = new EventLog();
const trace = new TraceRecorder();
const evidence: Evidence[] = [];OpenAI Agents SDK предлагает agents, agents-as-tools, handoffs, guardrails, sessions, human-in-the-loop и tracing; Claude Agent SDK даёт programmable agent loop на Python и TypeScript. Эти возможности используются через adapters, не отдавая SDK владение вашей domain database и необратимыми эффектами. Граница adapter узкая: нормализовать model output в typed result, превратить tool request в проверяемый host call, передать usage и timing в общий budget/trace, сериализовать pause/resume, но не решать domain acceptance внутри свободного текста.
Практический выигрыш. Стабильные outcome-, budget- и trace-схемы позволяют A/B-тестировать модели и фреймворки на одном task bank, не переписывая систему под каждый SDK.
Control plane развязан от провайдера. Дальше - три главы о конкретных продуктах, и первая различает Claude Code subagents и agent teams.