mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-11-09 22:13:39 +09:00
Merge branch 'pr-3389' into integ/train-3.45.1
This commit is contained in:
commit
012daa5fed
6 files changed
+648
-15
No files matched your search
@@ -0,0 +1,40 @@
|
||||
name: Resource admission
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'v3/@claude-flow/performance/**'
|
||||
- 'v3/docs/adr/ADR-400-task-conditioned-resource-admission.md'
|
||||
- '.github/workflows/resource-admission.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: resource-admission-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
name: Node ${{ matrix.node }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node: [22.22.3, 24.19.0]
|
||||
steps:
|
||||
- name: Check out exact source
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: npm
|
||||
- name: Install exact repository dependencies
|
||||
run: npm ci --ignore-scripts --no-audit --fund=false
|
||||
- name: Type check and build performance package
|
||||
run: npx tsc -p v3/@claude-flow/performance/tsconfig.json
|
||||
- name: Run resource admission tests and structural benchmarks
|
||||
run: npx vitest run v3/@claude-flow/performance/__tests__/resource-admission.test.ts v3/@claude-flow/performance/__tests__/resource-admission-report.test.ts
|
||||
@@ -0,0 +1,144 @@
|
||||
import { expect, it } from 'vitest';
|
||||
import { decideResourceAdmission, type ResourceAdmissionInput } from '../src/resource-admission.js';
|
||||
|
||||
const now = '2026-09-21T14:00:00.000Z';
|
||||
|
||||
function inputFor(seed: number, batch: number, index: number, unit: number): ResourceAdmissionInput {
|
||||
return {
|
||||
profile: {
|
||||
profileId: `profile.report.${seed}.${batch}.${index}`,
|
||||
toolId: 'tool.synthetic',
|
||||
profileDigest: 'a'.repeat(64),
|
||||
observedAt: '2026-09-21T13:59:30.000Z',
|
||||
cpuDemand: unit,
|
||||
memoryDemand: (unit * 1.7) % 1,
|
||||
ioDemand: (unit * 2.3) % 1,
|
||||
remoteWaitFraction: (unit * 3.1) % 1,
|
||||
minCpuCores: 1,
|
||||
maxCpuCores: 8,
|
||||
},
|
||||
host: {
|
||||
snapshotId: 'host.report',
|
||||
telemetryDigest: 'b'.repeat(64),
|
||||
observedAt: '2026-09-21T13:59:59.000Z',
|
||||
cpuPressure: (unit * 1.3) % 1,
|
||||
memoryPressure: (unit * 1.9) % 1,
|
||||
ioPressure: (unit * 2.7) % 1,
|
||||
allocatableCpuCores: 16,
|
||||
maxCpuPerTask: 8,
|
||||
},
|
||||
policy: {
|
||||
profileMaxAgeMs: 300_000,
|
||||
telemetryMaxAgeMs: 30_000,
|
||||
maxCpuPerTask: 8,
|
||||
cpuPressureDefer: 0.9,
|
||||
memoryPressureDefer: 0.9,
|
||||
ioPressureDefer: 0.9,
|
||||
safetyHeadroomFraction: 0.1,
|
||||
},
|
||||
requestedConcurrency: 1 + (index % 4),
|
||||
now,
|
||||
};
|
||||
}
|
||||
|
||||
function fixedCpuBaseline(input: ResourceAdmissionInput): number {
|
||||
const share = input.host.allocatableCpuCores * (1 - input.policy.safetyHeadroomFraction) / input.requestedConcurrency;
|
||||
return Math.max(0, Math.min(4, input.profile.maxCpuCores, input.host.maxCpuPerTask, input.policy.maxCpuPerTask, share));
|
||||
}
|
||||
|
||||
function percentile(values: number[], fraction: number): number {
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * fraction))];
|
||||
}
|
||||
|
||||
function mean(values: number[]): number {
|
||||
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
||||
}
|
||||
|
||||
function variance(values: number[]): number {
|
||||
const center = mean(values);
|
||||
return values.reduce((sum, value) => sum + (value - center) ** 2, 0) / values.length;
|
||||
}
|
||||
|
||||
it('reports baseline and candidate resource admission overhead with frozen workload', () => {
|
||||
const seeds = [11, 29, 47, 71, 101];
|
||||
const candidateBatchMicros: number[] = [];
|
||||
const baselineBatchMicros: number[] = [];
|
||||
let candidateCeilingViolations = 0;
|
||||
let baselineCeilingViolations = 0;
|
||||
let admits = 0;
|
||||
let defers = 0;
|
||||
let rejects = 0;
|
||||
let samples = 0;
|
||||
|
||||
for (const seed of seeds) {
|
||||
let state = seed >>> 0;
|
||||
for (let batch = 0; batch < 20; batch += 1) {
|
||||
const inputs: ResourceAdmissionInput[] = [];
|
||||
for (let index = 0; index < 100; index += 1) {
|
||||
state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
|
||||
inputs.push(inputFor(seed, batch, index, state / 0xffffffff));
|
||||
}
|
||||
|
||||
let start = performance.now();
|
||||
for (const input of inputs) {
|
||||
const cpu = fixedCpuBaseline(input);
|
||||
if (cpu > input.host.maxCpuPerTask || cpu > input.policy.maxCpuPerTask) baselineCeilingViolations += 1;
|
||||
}
|
||||
baselineBatchMicros.push(((performance.now() - start) * 1000) / inputs.length);
|
||||
|
||||
start = performance.now();
|
||||
for (const input of inputs) {
|
||||
const decision = decideResourceAdmission(input);
|
||||
samples += 1;
|
||||
if (decision.verdict === 'admit') admits += 1;
|
||||
if (decision.verdict === 'defer') defers += 1;
|
||||
if (decision.verdict === 'reject') rejects += 1;
|
||||
if (decision.recommendedCpuCores > input.host.maxCpuPerTask || decision.recommendedCpuCores > input.policy.maxCpuPerTask) {
|
||||
candidateCeilingViolations += 1;
|
||||
}
|
||||
}
|
||||
candidateBatchMicros.push(((performance.now() - start) * 1000) / inputs.length);
|
||||
}
|
||||
}
|
||||
|
||||
const baselineMean = mean(baselineBatchMicros);
|
||||
const candidateMean = mean(candidateBatchMicros);
|
||||
const report = {
|
||||
schema: 'ruflo-resource-admission-benchmark/v2',
|
||||
environment: { node: process.version, platform: process.platform, arch: process.arch },
|
||||
workload: '10000 deterministic synthetic resource decisions in 100 batches',
|
||||
seeds,
|
||||
sampleSize: samples,
|
||||
baseline: {
|
||||
name: 'fixed_cpu_clipped_to_host_ceiling',
|
||||
meanMicrosPerDecision: baselineMean,
|
||||
p95MicrosPerDecision: percentile(baselineBatchMicros, 0.95),
|
||||
varianceMicros2: variance(baselineBatchMicros),
|
||||
ceilingViolations: baselineCeilingViolations,
|
||||
},
|
||||
candidate: {
|
||||
name: 'task_conditioned_resource_admission',
|
||||
meanMicrosPerDecision: candidateMean,
|
||||
p95MicrosPerDecision: percentile(candidateBatchMicros, 0.95),
|
||||
varianceMicros2: variance(candidateBatchMicros),
|
||||
ceilingViolations: candidateCeilingViolations,
|
||||
admits,
|
||||
defers,
|
||||
rejects,
|
||||
},
|
||||
absoluteOverheadMicros: candidateMean - baselineMean,
|
||||
relativeOverheadRatio: candidateMean / Math.max(baselineMean, Number.EPSILON),
|
||||
ablations: ['remote_wait_cpu_reduction', 'cpu_pressure_defer', 'memory_pressure_defer', 'io_pressure_defer'],
|
||||
failures: 0,
|
||||
regressions: { taskQuality: 'not_measured_in_structural_benchmark', starvation: 'not_measured_in_structural_benchmark' },
|
||||
costUsd: 0,
|
||||
energy: 'not_measured',
|
||||
reproduction: 'npx vitest run v3/@claude-flow/performance/__tests__/resource-admission-report.test.ts',
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(report));
|
||||
expect(samples).toBe(10_000);
|
||||
expect(candidateCeilingViolations).toBe(0);
|
||||
expect(report.candidate.p95MicrosPerDecision).toBeLessThan(100);
|
||||
});
|
||||
@@ -0,0 +1,203 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { decideResourceAdmission, type ResourceAdmissionInput } from '../src/resource-admission.js';
|
||||
|
||||
const now = '2026-09-21T14:00:00.000Z';
|
||||
const digest = 'a'.repeat(64);
|
||||
|
||||
function fixture(overrides: Partial<ResourceAdmissionInput> = {}): ResourceAdmissionInput {
|
||||
const base: ResourceAdmissionInput = {
|
||||
profile: {
|
||||
profileId: 'profile.raqa.cpu.v1',
|
||||
toolId: 'retrieval.local',
|
||||
profileDigest: digest,
|
||||
observedAt: '2026-09-21T13:59:30.000Z',
|
||||
cpuDemand: 0.9,
|
||||
memoryDemand: 0.45,
|
||||
ioDemand: 0.2,
|
||||
remoteWaitFraction: 0.1,
|
||||
minCpuCores: 1,
|
||||
maxCpuCores: 8,
|
||||
},
|
||||
host: {
|
||||
snapshotId: 'host.primary',
|
||||
telemetryDigest: 'b'.repeat(64),
|
||||
observedAt: '2026-09-21T13:59:59.000Z',
|
||||
cpuPressure: 0.3,
|
||||
memoryPressure: 0.2,
|
||||
ioPressure: 0.2,
|
||||
allocatableCpuCores: 16,
|
||||
maxCpuPerTask: 8,
|
||||
},
|
||||
policy: {
|
||||
profileMaxAgeMs: 300_000,
|
||||
telemetryMaxAgeMs: 30_000,
|
||||
maxCpuPerTask: 8,
|
||||
cpuPressureDefer: 0.9,
|
||||
memoryPressureDefer: 0.9,
|
||||
ioPressureDefer: 0.9,
|
||||
safetyHeadroomFraction: 0.1,
|
||||
},
|
||||
requestedConcurrency: 1,
|
||||
now,
|
||||
};
|
||||
return {
|
||||
...base,
|
||||
...overrides,
|
||||
profile: { ...base.profile, ...(overrides.profile ?? {}) },
|
||||
host: { ...base.host, ...(overrides.host ?? {}) },
|
||||
policy: { ...base.policy, ...(overrides.policy ?? {}) },
|
||||
};
|
||||
}
|
||||
|
||||
describe('resource admission policy', () => {
|
||||
it('allocates more CPU to a measured CPU sensitive task without crossing ceilings', () => {
|
||||
const decision = decideResourceAdmission(fixture());
|
||||
expect(decision.verdict).toBe('admit');
|
||||
expect(decision.reasonCodes).toContain('CPU_SENSITIVE');
|
||||
expect(decision.recommendedCpuCores).toBeGreaterThan(1);
|
||||
expect(decision.recommendedCpuCores).toBeLessThanOrEqual(8);
|
||||
expect(decision.authority).toBe('none');
|
||||
});
|
||||
|
||||
it('does not waste CPU on a remote wait dominated task', () => {
|
||||
const input = fixture({
|
||||
profile: {
|
||||
...fixture().profile,
|
||||
profileId: 'profile.web.remote.v1',
|
||||
cpuDemand: 0.7,
|
||||
remoteWaitFraction: 0.9,
|
||||
},
|
||||
});
|
||||
const decision = decideResourceAdmission(input);
|
||||
expect(decision.verdict).toBe('admit');
|
||||
expect(decision.recommendedCpuCores).toBe(input.profile.minCpuCores);
|
||||
expect(decision.limitingResource).toBe('remote_wait');
|
||||
});
|
||||
|
||||
it('defers CPU sensitive work during high CPU pressure', () => {
|
||||
const decision = decideResourceAdmission(fixture({
|
||||
host: { ...fixture().host, cpuPressure: 0.95 },
|
||||
}));
|
||||
expect(decision.verdict).toBe('defer');
|
||||
expect(decision.reasonCodes).toEqual(['CPU_PRESSURE']);
|
||||
expect(decision.recommendedCpuCores).toBe(0);
|
||||
});
|
||||
|
||||
it('defers memory heavy work during memory pressure', () => {
|
||||
const decision = decideResourceAdmission(fixture({
|
||||
profile: { ...fixture().profile, memoryDemand: 0.9 },
|
||||
host: { ...fixture().host, memoryPressure: 0.95 },
|
||||
}));
|
||||
expect(decision.verdict).toBe('defer');
|
||||
expect(decision.reasonCodes).toEqual(['MEMORY_PRESSURE']);
|
||||
});
|
||||
|
||||
it('fails stale telemetry closed', () => {
|
||||
const decision = decideResourceAdmission(fixture({
|
||||
host: { ...fixture().host, observedAt: '2026-09-21T13:00:00.000Z' },
|
||||
}));
|
||||
expect(decision.verdict).toBe('defer');
|
||||
expect(decision.reasonCodes).toEqual(['STALE_TELEMETRY']);
|
||||
});
|
||||
|
||||
it('rejects an impossible policy ceiling', () => {
|
||||
const decision = decideResourceAdmission(fixture({
|
||||
profile: { ...fixture().profile, minCpuCores: 4, maxCpuCores: 8 },
|
||||
policy: { ...fixture().policy, maxCpuPerTask: 2 },
|
||||
}));
|
||||
expect(decision.verdict).toBe('reject');
|
||||
expect(decision.reasonCodes).toEqual(['POLICY_CEILING_BELOW_PROFILE_MINIMUM']);
|
||||
});
|
||||
|
||||
it('never recommends more aggregate CPU than current allocatable capacity', () => {
|
||||
const input = fixture({
|
||||
host: { ...fixture().host, allocatableCpuCores: 4, maxCpuPerTask: 8 },
|
||||
requestedConcurrency: 4,
|
||||
policy: { ...fixture().policy, safetyHeadroomFraction: 0 },
|
||||
});
|
||||
const decision = decideResourceAdmission(input);
|
||||
expect(decision.verdict).toBe('admit');
|
||||
expect(decision.recommendedCpuCores * input.requestedConcurrency).toBeLessThanOrEqual(input.host.allocatableCpuCores);
|
||||
});
|
||||
|
||||
it('is deterministic for identical evidence', () => {
|
||||
const input = fixture();
|
||||
const first = decideResourceAdmission(input);
|
||||
const second = decideResourceAdmission(input);
|
||||
expect(second).toEqual(first);
|
||||
expect(first.evidenceDigest).toMatch(/^[0-9a-f]{16}$/);
|
||||
});
|
||||
|
||||
it('rejects malformed evidence rather than widening allocation', () => {
|
||||
const input = fixture({
|
||||
profile: { ...fixture().profile, profileDigest: 'not-a-digest' },
|
||||
});
|
||||
const decision = decideResourceAdmission(input);
|
||||
expect(decision.verdict).toBe('reject');
|
||||
expect(decision.recommendedCpuCores).toBe(0);
|
||||
expect(decision.authority).toBe('none');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resource admission structural benchmark', () => {
|
||||
it('keeps p95 policy decision overhead below 100 microseconds', () => {
|
||||
const seeds = [11, 29, 47, 71, 101];
|
||||
const batchMicros: number[] = [];
|
||||
let ceilingViolations = 0;
|
||||
let decisions = 0;
|
||||
|
||||
for (const seed of seeds) {
|
||||
let state = seed >>> 0;
|
||||
for (let batch = 0; batch < 20; batch += 1) {
|
||||
const inputs: ResourceAdmissionInput[] = [];
|
||||
for (let index = 0; index < 100; index += 1) {
|
||||
state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
|
||||
const unit = state / 0xffffffff;
|
||||
inputs.push(fixture({
|
||||
profile: {
|
||||
...fixture().profile,
|
||||
profileId: `profile.synthetic.${seed}.${batch}.${index}`,
|
||||
cpuDemand: unit,
|
||||
memoryDemand: (unit * 1.7) % 1,
|
||||
ioDemand: (unit * 2.3) % 1,
|
||||
remoteWaitFraction: (unit * 3.1) % 1,
|
||||
},
|
||||
host: {
|
||||
...fixture().host,
|
||||
cpuPressure: (unit * 1.3) % 1,
|
||||
memoryPressure: (unit * 1.9) % 1,
|
||||
ioPressure: (unit * 2.7) % 1,
|
||||
},
|
||||
}));
|
||||
}
|
||||
const start = performance.now();
|
||||
for (const input of inputs) {
|
||||
const result = decideResourceAdmission(input);
|
||||
decisions += 1;
|
||||
if (result.recommendedCpuCores > input.host.maxCpuPerTask || result.recommendedCpuCores > input.policy.maxCpuPerTask) {
|
||||
ceilingViolations += 1;
|
||||
}
|
||||
}
|
||||
batchMicros.push(((performance.now() - start) * 1000) / inputs.length);
|
||||
}
|
||||
}
|
||||
|
||||
batchMicros.sort((a, b) => a - b);
|
||||
const p95 = batchMicros[Math.floor(batchMicros.length * 0.95)];
|
||||
console.log(JSON.stringify({
|
||||
schema: 'ruflo-resource-admission-benchmark/v1',
|
||||
workload: '10000 deterministic synthetic resource decisions',
|
||||
seeds,
|
||||
sampleSize: decisions,
|
||||
metric: 'microseconds_per_decision',
|
||||
p95,
|
||||
ceilingViolations,
|
||||
authorityExpansion: 0,
|
||||
energy: 'not_measured',
|
||||
costUsd: 0,
|
||||
}));
|
||||
expect(decisions).toBe(10_000);
|
||||
expect(ceilingViolations).toBe(0);
|
||||
expect(p95).toBeLessThan(100);
|
||||
});
|
||||
});
|
||||
@@ -2,20 +2,10 @@
|
||||
* @claude-flow/performance
|
||||
*
|
||||
* Performance module for claude-flow v3.
|
||||
* Provides benchmarking, Flash Attention validation, and optimization utilities.
|
||||
*
|
||||
* Target Performance Metrics:
|
||||
* - CLI Startup: <500ms (5x faster)
|
||||
* - MCP Init: <400ms (4.5x faster)
|
||||
* - Agent Spawn: <200ms (4x faster)
|
||||
* - Vector Search: <1ms (150x faster)
|
||||
* - Memory Write: <5ms (10x faster)
|
||||
* - Swarm Consensus: <100ms (5x faster)
|
||||
* - Flash Attention: 2.49x-7.47x speedup
|
||||
* - Memory Usage: <256MB (50% reduction)
|
||||
* Provides benchmarking, Flash Attention validation, optimization, and
|
||||
* resource-aware admission primitives.
|
||||
*/
|
||||
|
||||
// Re-export benchmark framework
|
||||
export {
|
||||
benchmark,
|
||||
BenchmarkRunner,
|
||||
@@ -34,7 +24,6 @@ export {
|
||||
type PerformanceTarget,
|
||||
} from './framework/benchmark.js';
|
||||
|
||||
// Re-export Flash Attention integration
|
||||
export {
|
||||
FlashAttentionOptimizer,
|
||||
createFlashAttentionOptimizer,
|
||||
@@ -45,7 +34,6 @@ export {
|
||||
type PerformanceMetrics as AttentionMetrics,
|
||||
} from './attention-integration.js';
|
||||
|
||||
// Re-export Flash Attention benchmarks
|
||||
export {
|
||||
AttentionBenchmarkRunner,
|
||||
formatBenchmarkTable,
|
||||
@@ -59,5 +47,15 @@ export {
|
||||
type MemoryProfile,
|
||||
} from './attention-benchmarks.js';
|
||||
|
||||
// Default export for convenience
|
||||
export {
|
||||
decideResourceAdmission,
|
||||
type ResourceVerdict,
|
||||
type ResourceLimit,
|
||||
type ResourceProfile,
|
||||
type ResourceHostSnapshot,
|
||||
type ResourcePolicy,
|
||||
type ResourceAdmissionInput,
|
||||
type ResourceAdmissionDecision,
|
||||
} from './resource-admission.js';
|
||||
|
||||
export { default } from './framework/benchmark.js';
|
||||
@@ -0,0 +1,154 @@
|
||||
export type ResourceVerdict = 'admit' | 'defer' | 'reject';
|
||||
export type ResourceLimit = 'cpu' | 'memory' | 'io' | 'remote_wait' | 'capacity' | 'telemetry' | 'profile' | 'none';
|
||||
|
||||
export interface ResourceProfile {
|
||||
profileId: string;
|
||||
toolId: string;
|
||||
profileDigest: string;
|
||||
observedAt: string;
|
||||
cpuDemand: number;
|
||||
memoryDemand: number;
|
||||
ioDemand: number;
|
||||
remoteWaitFraction: number;
|
||||
minCpuCores: number;
|
||||
maxCpuCores: number;
|
||||
}
|
||||
|
||||
export interface ResourceHostSnapshot {
|
||||
snapshotId: string;
|
||||
telemetryDigest: string;
|
||||
observedAt: string;
|
||||
cpuPressure: number;
|
||||
memoryPressure: number;
|
||||
ioPressure: number;
|
||||
allocatableCpuCores: number;
|
||||
maxCpuPerTask: number;
|
||||
}
|
||||
|
||||
export interface ResourcePolicy {
|
||||
profileMaxAgeMs: number;
|
||||
telemetryMaxAgeMs: number;
|
||||
maxCpuPerTask: number;
|
||||
cpuPressureDefer: number;
|
||||
memoryPressureDefer: number;
|
||||
ioPressureDefer: number;
|
||||
safetyHeadroomFraction: number;
|
||||
}
|
||||
|
||||
export interface ResourceAdmissionInput {
|
||||
profile: ResourceProfile;
|
||||
host: ResourceHostSnapshot;
|
||||
policy: ResourcePolicy;
|
||||
requestedConcurrency: number;
|
||||
now: string;
|
||||
}
|
||||
|
||||
export interface ResourceAdmissionDecision {
|
||||
verdict: ResourceVerdict;
|
||||
recommendedCpuCores: number;
|
||||
limitingResource: ResourceLimit;
|
||||
reasonCodes: readonly string[];
|
||||
evidenceDigest: string;
|
||||
authority: 'none';
|
||||
}
|
||||
|
||||
const ID = /^[A-Za-z0-9._:/-]{1,160}$/;
|
||||
const HEX64 = /^[0-9a-f]{64}$/;
|
||||
|
||||
export function decideResourceAdmission(input: ResourceAdmissionInput): ResourceAdmissionDecision {
|
||||
const invalid = validate(input);
|
||||
if (invalid) return finish('reject', 0, 'profile', [invalid], `invalid:${invalid}`);
|
||||
|
||||
const now = Date.parse(input.now);
|
||||
const profileAge = now - Date.parse(input.profile.observedAt);
|
||||
const telemetryAge = now - Date.parse(input.host.observedAt);
|
||||
if (profileAge < -1000) return make(input, 'reject', 0, 'profile', ['PROFILE_FROM_FUTURE']);
|
||||
if (telemetryAge < -1000) return make(input, 'reject', 0, 'telemetry', ['TELEMETRY_FROM_FUTURE']);
|
||||
if (profileAge > input.policy.profileMaxAgeMs) return make(input, 'defer', 0, 'profile', ['STALE_PROFILE']);
|
||||
if (telemetryAge > input.policy.telemetryMaxAgeMs) return make(input, 'defer', 0, 'telemetry', ['STALE_TELEMETRY']);
|
||||
|
||||
const hardPolicyCeiling = Math.min(input.host.maxCpuPerTask, input.policy.maxCpuPerTask);
|
||||
if (hardPolicyCeiling < input.profile.minCpuCores) {
|
||||
return make(input, 'reject', 0, 'capacity', ['POLICY_CEILING_BELOW_PROFILE_MINIMUM']);
|
||||
}
|
||||
|
||||
const usableCpu = input.host.allocatableCpuCores * (1 - input.policy.safetyHeadroomFraction);
|
||||
const concurrencyShare = usableCpu / input.requestedConcurrency;
|
||||
const ceiling = Math.min(input.profile.maxCpuCores, hardPolicyCeiling, concurrencyShare);
|
||||
if (ceiling < input.profile.minCpuCores) {
|
||||
return make(input, 'defer', 0, 'capacity', ['CONCURRENCY_CAPACITY_EXHAUSTED']);
|
||||
}
|
||||
|
||||
const cpuSensitivity = clamp(input.profile.cpuDemand * (1 - 0.85 * input.profile.remoteWaitFraction));
|
||||
if (input.profile.memoryDemand >= 0.4 && input.host.memoryPressure >= input.policy.memoryPressureDefer) {
|
||||
return make(input, 'defer', 0, 'memory', ['MEMORY_PRESSURE']);
|
||||
}
|
||||
if (input.profile.ioDemand >= 0.4 && input.host.ioPressure >= input.policy.ioPressureDefer) {
|
||||
return make(input, 'defer', 0, 'io', ['IO_PRESSURE']);
|
||||
}
|
||||
if (cpuSensitivity >= 0.35 && input.host.cpuPressure >= input.policy.cpuPressureDefer) {
|
||||
return make(input, 'defer', 0, 'cpu', ['CPU_PRESSURE']);
|
||||
}
|
||||
|
||||
const reasons: string[] = [];
|
||||
if (input.profile.remoteWaitFraction >= 0.7) reasons.push('REMOTE_WAIT_DOMINANT');
|
||||
if (cpuSensitivity >= 0.6) reasons.push('CPU_SENSITIVE');
|
||||
if (reasons.length === 0) reasons.push('WITHIN_RESOURCE_ENVELOPE');
|
||||
|
||||
const target = input.profile.remoteWaitFraction >= 0.7
|
||||
? input.profile.minCpuCores
|
||||
: input.profile.minCpuCores + (ceiling - input.profile.minCpuCores) * cpuSensitivity;
|
||||
const cpu = Math.min(ceiling, Math.ceil(target * 4) / 4);
|
||||
return make(input, 'admit', cpu, limitingResource(input, cpuSensitivity), reasons);
|
||||
}
|
||||
|
||||
function validate(input: ResourceAdmissionInput): string | undefined {
|
||||
if (!input || typeof input !== 'object' || !input.profile || !input.host || !input.policy) return 'INVALID_INPUT';
|
||||
if (!ID.test(input.profile.profileId) || !ID.test(input.profile.toolId) || !ID.test(input.host.snapshotId)) return 'INVALID_ID';
|
||||
if (!HEX64.test(input.profile.profileDigest) || !HEX64.test(input.host.telemetryDigest)) return 'INVALID_DIGEST';
|
||||
if (![input.profile.observedAt, input.host.observedAt, input.now].every(canonicalTime)) return 'INVALID_TIMESTAMP';
|
||||
const units = [input.profile.cpuDemand, input.profile.memoryDemand, input.profile.ioDemand, input.profile.remoteWaitFraction, input.host.cpuPressure, input.host.memoryPressure, input.host.ioPressure, input.policy.cpuPressureDefer, input.policy.memoryPressureDefer, input.policy.ioPressureDefer, input.policy.safetyHeadroomFraction];
|
||||
if (units.some((v) => !Number.isFinite(v) || v < 0 || v > 1)) return 'INVALID_UNIT_VALUE';
|
||||
if (input.policy.safetyHeadroomFraction > 0.5) return 'EXCESSIVE_HEADROOM';
|
||||
const cores = [input.profile.minCpuCores, input.profile.maxCpuCores, input.host.allocatableCpuCores, input.host.maxCpuPerTask, input.policy.maxCpuPerTask];
|
||||
if (cores.some((v) => !Number.isFinite(v) || v <= 0 || v > 4096)) return 'INVALID_CPU_BOUND';
|
||||
if (input.profile.minCpuCores > input.profile.maxCpuCores) return 'INVALID_PROFILE_CPU_RANGE';
|
||||
if (!Number.isSafeInteger(input.requestedConcurrency) || input.requestedConcurrency < 1 || input.requestedConcurrency > 100000) return 'INVALID_CONCURRENCY';
|
||||
if (!Number.isSafeInteger(input.policy.profileMaxAgeMs) || input.policy.profileMaxAgeMs < 1 || !Number.isSafeInteger(input.policy.telemetryMaxAgeMs) || input.policy.telemetryMaxAgeMs < 1) return 'INVALID_FRESHNESS_BOUND';
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function make(input: ResourceAdmissionInput, verdict: ResourceVerdict, cpu: number, limit: ResourceLimit, reasons: readonly string[]): ResourceAdmissionDecision {
|
||||
const material = [input.profile.profileId, input.profile.toolId, input.profile.profileDigest, input.host.snapshotId, input.host.telemetryDigest, input.profile.observedAt, input.host.observedAt, input.now, input.requestedConcurrency, verdict, cpu, limit, reasons.join(',')].join('|');
|
||||
return finish(verdict, cpu, limit, reasons, material);
|
||||
}
|
||||
|
||||
function finish(verdict: ResourceVerdict, cpu: number, limit: ResourceLimit, reasons: readonly string[], material: string): ResourceAdmissionDecision {
|
||||
return Object.freeze({ verdict, recommendedCpuCores: cpu, limitingResource: limit, reasonCodes: Object.freeze([...reasons]), evidenceDigest: stableFingerprint(material), authority: 'none' as const });
|
||||
}
|
||||
|
||||
function limitingResource(input: ResourceAdmissionInput, cpuSensitivity: number): ResourceLimit {
|
||||
if (input.profile.remoteWaitFraction >= 0.7) return 'remote_wait';
|
||||
const values: Array<[ResourceLimit, number]> = [['cpu', cpuSensitivity * Math.max(0.1, input.host.cpuPressure)], ['memory', input.profile.memoryDemand * Math.max(0.1, input.host.memoryPressure)], ['io', input.profile.ioDemand * Math.max(0.1, input.host.ioPressure)]];
|
||||
return values.sort((a, b) => b[1] - a[1])[0][0];
|
||||
}
|
||||
|
||||
function canonicalTime(value: string): boolean {
|
||||
if (typeof value !== 'string') return false;
|
||||
const parsed = new Date(value);
|
||||
return Number.isFinite(parsed.getTime()) && parsed.toISOString() === value;
|
||||
}
|
||||
|
||||
function clamp(value: number): number { return Math.max(0, Math.min(1, value)); }
|
||||
|
||||
/** Stable evidence fingerprint only. It is not a signature or authority token. */
|
||||
function stableFingerprint(value: string): string {
|
||||
let a = 2166136261 >>> 0;
|
||||
let b = 2246822519 >>> 0;
|
||||
for (let i = 0; i < value.length; i += 1) {
|
||||
const c = value.charCodeAt(i);
|
||||
a = Math.imul(a ^ c, 16777619) >>> 0;
|
||||
b = Math.imul(b ^ c, 3266489917) >>> 0;
|
||||
}
|
||||
return `${a.toString(16).padStart(8, '0')}${b.toString(16).padStart(8, '0')}`;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
# ADR-400: Task-Conditioned Resource Admission
|
||||
|
||||
Status: proposed
|
||||
|
||||
Date: 2026-09-21
|
||||
|
||||
Issue: #3388
|
||||
|
||||
## Context
|
||||
|
||||
Agent serving mixes remote model latency with local tool execution, so end-to-end task latency is a poor proxy for local resource demand. `Not All AI Agents Are Equal: Characterizing Resource and Performance Dynamics` (arXiv:2609.19947, submitted 2026-09-17) reports task-dependent CPU, memory, disk-I/O, and remote-wait behavior across retrieval-augmented QA, web search, and coding. The originating team reports CPU-aware tool admission improving CPU-sensitive task latency by up to about 5.4x and task-aware CPU allocation reducing mixed-workload average latency by about 32% versus its native-agent baseline.
|
||||
|
||||
The paper is an originating-team result, not an independently reproduced RuFlo claim. Its systems measurements replay recorded Gemini 3 Flash trajectories on an Azure VM with 24 vCPU AMD EPYC 7V13 and 216 GB RAM. GPU inference is out of scope because the model is accessed remotely.
|
||||
|
||||
RuFlo already contains generic load-balancing and resource-allocation guidance. The missing reusable primitive is a deterministic policy that separates measured task/tool resource phenotype from current host pressure and from host-supplied safety ceilings.
|
||||
|
||||
## Decision
|
||||
|
||||
Add a pure `ResourceAdmissionPolicy` primitive to `@claude-flow/performance`.
|
||||
|
||||
The policy consumes a provenance-bound resource profile, a fresh host telemetry snapshot, a fixed host policy, and requested concurrency. It returns only an admission recommendation and CPU allocation recommendation.
|
||||
|
||||
It does not execute a task, mutate container limits, kill another task, alter RVM authority, or widen a host ceiling. Every decision carries `authority: none`.
|
||||
|
||||
CPU allocation is reduced for remote-wait-dominant work because additional local cores are unlikely to improve the critical path. CPU-sensitive work receives more of the available per-task envelope when pressure allows it. High CPU, memory, or I/O pressure causes defer decisions for workloads sensitive to the pressured resource. Stale evidence fails closed.
|
||||
|
||||
The decision fingerprint is deterministic but intentionally non-authoritative and non-cryptographic. Production provenance or signing belongs at the RVF/RVM boundary.
|
||||
|
||||
## Invariants
|
||||
|
||||
1. A recommendation never exceeds the profile, host, or policy CPU ceiling.
|
||||
2. Aggregate recommendation under declared concurrency never exceeds current allocatable CPU after configured headroom.
|
||||
3. Stale or future evidence cannot result in an expanded allocation.
|
||||
4. Malformed profile or telemetry evidence is rejected.
|
||||
5. Natural-language task descriptions are not policy input.
|
||||
6. The primitive grants no execution capability and carries no authority.
|
||||
7. Resource history from RuVector may inform a future profile estimator, but retrieval confidence cannot override a host ceiling.
|
||||
8. A task-conditioned policy cannot preempt an incumbent task; destructive conflict resolution remains a separate RVM-controlled decision.
|
||||
|
||||
## Falsification
|
||||
|
||||
Independent MetaHarness evaluation must compare fixed allocation, generic utilization-only allocation, task-conditioned allocation, and a retrospective oracle under matched model, workload, host, resource budget, and evaluator conditions.
|
||||
|
||||
Required workloads include CPU-bound retrieval, remote-wait-heavy search, I/O-heavy coding, bursty memory workloads, malformed profiles, stale telemetry, resource exhaustion, and adversarial attempts to exceed a ceiling.
|
||||
|
||||
Reject this architecture if generic utilization-only control matches it within noise, if a stale profile causes unsafe admission, or if gains on one workload come from starving another.
|
||||
|
||||
## Benchmark contract
|
||||
|
||||
Report baseline and candidate, exact commit, hardware, operating system, Node version, workload, model/API version where relevant, seeds, sample size, p50 and p95 latency, throughput, CPU-seconds per completed task, memory peak, disk I/O, task success, failures, variance, cost, energy where measurable, ablations, and reproduction commands.
|
||||
|
||||
The pure policy benchmark requires zero ceiling violations, zero non-deterministic decisions, and less than 100 microseconds p95 decision overhead.
|
||||
|
||||
System-level promotion requires at least one of:
|
||||
|
||||
1. at least 15% lower p95 tool latency;
|
||||
2. at least 15% higher throughput at the same resource budget;
|
||||
3. at least 20% lower CPU-seconds per completed task;
|
||||
|
||||
with no task-success regression greater than 2 absolute points and no protected fairness or starvation regression.
|
||||
|
||||
## Cross-stack mapping
|
||||
|
||||
RuFlo consumes the policy for future scheduling and admission.
|
||||
|
||||
MetaHarness owns independent systems evaluation and protected fairness tests.
|
||||
|
||||
RVM remains the effect and resource-ceiling enforcement boundary.
|
||||
|
||||
Core Memory stores accepted profiles and benchmark evidence with provenance.
|
||||
|
||||
RuVector and RuVector WASM may retrieve historical resource phenotypes but never grant allocation authority.
|
||||
|
||||
RVF binds profile, telemetry, decision, and evaluation digests. RVForge packages only reproduced policy artifacts.
|
||||
|
||||
Autogenous and Dream Machine may improve profile estimation after baselines freeze, but cannot modify resource ceilings or acceptance thresholds.
|
||||
|
||||
MidStream carries telemetry and phase-boundary observations.
|
||||
|
||||
RuView, RuField, and WorldGraph can use the same primitive for sensing and spatial pipelines where task resource profiles differ materially.
|
||||
|
||||
LatentMesh can contribute distributed pressure observations without creating trust.
|
||||
|
||||
Cognitum can use accepted policy behavior to improve tenant density, latency, and gross margin under explicit SLA ceilings.
|
||||
|
||||
MCP supplies tool identity; host-bound policy decides local admission.
|
||||
|
||||
## Migration and rollback
|
||||
|
||||
The change is additive. Existing scheduling behavior remains unchanged until a later, separately reviewed integration chooses to consume the policy. Rollback removes the module, export, tests, workflow, and ADR. No durable schema migration is required.
|
||||
|
||||
## Governance
|
||||
|
||||
No autonomous merge, deployment, scheduler integration, resource-limit mutation, credential escalation, or weakening of admission and evaluation gates is permitted. Independent MetaHarness evidence and human approval are required before integration.
|
||||
Reference in new issue
Block a user